neo4j-viz 0.7.0__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import warnings
4
- from collections.abc import Iterable
5
- from typing import Any, Callable, Hashable, Optional, Union
4
+ from collections.abc import Hashable, Iterable
5
+ from typing import Any, Callable
6
6
 
7
7
  from IPython.display import HTML
8
8
  from pydantic.alias_generators import to_snake
@@ -22,7 +22,6 @@ from .options import (
22
22
  from .relationship import Relationship
23
23
 
24
24
 
25
- # TODO helper for map properties to fields. helper for set caption (simplicity)
26
25
  class VisualizationGraph:
27
26
  """
28
27
  A graph to visualize.
@@ -86,13 +85,13 @@ class VisualizationGraph:
86
85
 
87
86
  def render(
88
87
  self,
89
- layout: Optional[Layout] = None,
90
- layout_options: Union[dict[str, Any], LayoutOptions, None] = None,
88
+ layout: Layout | None = None,
89
+ layout_options: dict[str, Any] | LayoutOptions | None = None,
91
90
  renderer: Renderer = Renderer.CANVAS,
92
91
  width: str = "100%",
93
92
  height: str = "600px",
94
- pan_position: Optional[tuple[float, float]] = None,
95
- initial_zoom: Optional[float] = None,
93
+ pan_position: tuple[float, float] | None = None,
94
+ initial_zoom: float | None = None,
96
95
  min_zoom: float = 0.075,
97
96
  max_zoom: float = 10,
98
97
  allow_dynamic_min_zoom: bool = True,
@@ -197,8 +196,8 @@ class VisualizationGraph:
197
196
  def set_node_captions(
198
197
  self,
199
198
  *,
200
- field: Optional[str] = None,
201
- property: Optional[str] = None,
199
+ field: str | None = None,
200
+ property: str | None = None,
202
201
  override: bool = True,
203
202
  ) -> None:
204
203
  """
@@ -265,9 +264,9 @@ class VisualizationGraph:
265
264
 
266
265
  def resize_nodes(
267
266
  self,
268
- sizes: Optional[dict[NodeIdType, RealNumber]] = None,
269
- node_radius_min_max: Optional[tuple[RealNumber, RealNumber]] = (3, 60),
270
- property: Optional[str] = None,
267
+ sizes: dict[NodeIdType, RealNumber] | None = None,
268
+ node_radius_min_max: tuple[RealNumber, RealNumber] | None = (3, 60),
269
+ property: str | None = None,
271
270
  ) -> None:
272
271
  """
273
272
  Resize the nodes in the graph.
@@ -334,6 +333,57 @@ class VisualizationGraph:
334
333
 
335
334
  node.size = size
336
335
 
336
+ def resize_relationships(
337
+ self,
338
+ widths: dict[str | int, RealNumber] | None = None,
339
+ property: str | None = None,
340
+ ) -> None:
341
+ """
342
+ Resize the width of relationships in the graph.
343
+
344
+ Parameters
345
+ ----------
346
+ widths:
347
+ A dictionary mapping from relationship ID to the new width of the relationship.
348
+ If a relationship ID is not in the dictionary, the width of the relationship is not changed.
349
+ Must be None if `property` is provided.
350
+ property:
351
+ The property of the relationships to use for sizing. Must be None if `widths` is provided.
352
+ """
353
+ if widths is not None and property is not None:
354
+ raise ValueError("At most one of the arguments `widths` and `property` can be provided")
355
+
356
+ if widths is None and property is None:
357
+ raise ValueError("At least one of `widths` or `property` must be given")
358
+
359
+ # Gather relationship widths
360
+ all_widths = {}
361
+ if widths is not None:
362
+ for rel in self.relationships:
363
+ width = widths.get(rel.id, rel.width)
364
+ if width is not None:
365
+ all_widths[rel.id] = width
366
+ elif property is not None:
367
+ for rel in self.relationships:
368
+ width = rel.properties.get(property, rel.width)
369
+ if width is not None:
370
+ all_widths[rel.id] = width
371
+
372
+ # Validate and apply relationship widths
373
+ for rel in self.relationships:
374
+ width = all_widths.get(rel.id)
375
+
376
+ if width is None:
377
+ continue
378
+
379
+ if not isinstance(width, (int, float)):
380
+ raise ValueError(f"Width for relationship '{rel.id}' must be a real number, but was {width}")
381
+
382
+ if width <= 0:
383
+ raise ValueError(f"Width for relationship '{rel.id}' must be positive, but was {width}")
384
+
385
+ rel.width = width
386
+
337
387
  @staticmethod
338
388
  def _normalize_values(
339
389
  node_map: dict[NodeIdType, RealNumber], min_max: tuple[float, float] = (0, 1)
@@ -359,9 +409,9 @@ class VisualizationGraph:
359
409
  def color_nodes(
360
410
  self,
361
411
  *,
362
- field: Optional[str] = None,
363
- property: Optional[str] = None,
364
- colors: Optional[ColorsType] = None,
412
+ field: str | None = None,
413
+ property: str | None = None,
414
+ colors: ColorsType | None = None,
365
415
  color_space: ColorSpace = ColorSpace.DISCRETE,
366
416
  override: bool = True,
367
417
  ) -> None:
@@ -406,6 +456,7 @@ class VisualizationGraph:
406
456
  >>> VG = VisualizationGraph(nodes=nodes)
407
457
 
408
458
  Color nodes based on a discrete field such as "label":
459
+
409
460
  >>> VG.color_nodes(field="label", color_space=ColorSpace.DISCRETE)
410
461
 
411
462
  Color nodes based on a continuous field such as "score":
@@ -428,6 +479,7 @@ class VisualizationGraph:
428
479
 
429
480
  def node_to_attr(node: Node) -> Any:
430
481
  return node.properties.get(attribute)
482
+
431
483
  else:
432
484
  assert field is not None
433
485
  attribute = to_snake(field)
@@ -456,39 +508,151 @@ class VisualizationGraph:
456
508
  }
457
509
 
458
510
  if isinstance(colors, dict):
459
- self._color_nodes_dict(colors, override, node_to_attr)
511
+ self._color_items_dict(self.nodes, colors, override, node_to_attr)
460
512
  else:
461
- self._color_nodes_iter(attribute, colors, override, node_to_attr)
513
+ self._color_items_iter(self.nodes, attribute, colors, override, node_to_attr)
462
514
 
463
- def _color_nodes_dict(
464
- self, colors: dict[str, ColorType], override: bool, node_to_attr: Callable[[Node], Any]
515
+ def color_relationships(
516
+ self,
517
+ *,
518
+ field: str | None = None,
519
+ property: str | None = None,
520
+ colors: ColorsType | None = None,
521
+ color_space: ColorSpace = ColorSpace.DISCRETE,
522
+ override: bool = True,
465
523
  ) -> None:
466
- for node in self.nodes:
467
- color = colors.get(node_to_attr(node))
524
+ """
525
+ Color the relationships in the graph based on either a relationship field, or a relationship property.
526
+
527
+ It's possible to color the relationships based on a discrete or continuous color space. In the discrete case,
528
+ a new color from the `colors` provided is assigned to each unique value of the relationship field/property.
529
+ In the continuous case, the `colors` should be a list of colors representing a range that are used to
530
+ create a gradient of colors based on the values of the relationship field/property.
531
+
532
+ Parameters
533
+ ----------
534
+ field:
535
+ The field of the relationships to base the coloring on. The type of this field must be hashable, or be a
536
+ list, set or dict containing only hashable types. Must be None if `property` is provided.
537
+ property:
538
+ The property of the relationships to base the coloring on. The type of this property must be hashable, or be a
539
+ list, set or dict containing only hashable types. Must be None if `field` is provided.
540
+ colors:
541
+ The colors to use for the relationships.
542
+ If `color_space` is `ColorSpace.DISCRETE`, the colors can be a dictionary mapping from field/property value
543
+ to color, or an iterable of colors in which case the colors are used in order.
544
+ If `color_space` is `ColorSpace.CONTINUOUS`, the colors must be a list of colors representing a range.
545
+ Allowed color values are for example “#FF0000”, “red” or (255, 0, 0) (full list: https://docs.pydantic.dev/2.0/usage/types/extra_types/color_types/).
546
+ The default colors are the Neo4j graph colors.
547
+ color_space:
548
+ The type of space of the provided `colors`. Either `ColorSpace.DISCRETE` or `ColorSpace.CONTINUOUS`. It determines whether
549
+ colors are assigned based on unique field/property values or a gradient of the values of the field/property.
550
+ override:
551
+ Whether to override existing colors of the relationships, if they have any.
552
+
553
+ Examples
554
+ --------
555
+
556
+ Given a VisualizationGraph `VG`:
557
+
558
+ >>> nodes = [Node(id="0"), Node(id="1")]
559
+ >>> relationships = [
560
+ ... Relationship(source="0", target="1", caption="ACTED_IN", properties={"score": 10}),
561
+ ... Relationship(source="1", target="0", caption="DIRECTED", properties={"score": 20}),
562
+ ... ]
563
+ >>> VG = VisualizationGraph(nodes=nodes, relationships=relationships)
564
+
565
+ Color relationships based on a discrete field such as "caption":
566
+
567
+ >>> VG.color_relationships(field="caption", color_space=ColorSpace.DISCRETE)
568
+
569
+ Color relationships based on a continuous field such as "score":
570
+
571
+ >>> VG.color_relationships(property="score", color_space=ColorSpace.CONTINUOUS)
572
+ """
573
+ if not ((field is None) ^ (property is None)):
574
+ raise ValueError(
575
+ f"Exactly one of the arguments `field` (received '{field}') and `property` (received '{property}') must be provided"
576
+ )
577
+
578
+ if field is None:
579
+ assert property is not None
580
+ attribute = property
581
+
582
+ def rel_to_attr(rel: Relationship) -> Any:
583
+ return rel.properties.get(attribute)
584
+
585
+ else:
586
+ assert field is not None
587
+ attribute = to_snake(field)
588
+
589
+ def rel_to_attr(rel: Relationship) -> Any:
590
+ return getattr(rel, attribute)
591
+
592
+ if color_space == ColorSpace.DISCRETE:
593
+ if colors is None:
594
+ colors = NEO4J_COLORS_DISCRETE
595
+ else:
596
+ rel_map = {rel.id: rel_to_attr(rel) for rel in self.relationships if rel_to_attr(rel) is not None}
597
+ normalized_map = self._normalize_values(rel_map)
598
+
599
+ if colors is None:
600
+ colors = NEO4J_COLORS_CONTINUOUS
601
+
602
+ if not isinstance(colors, list):
603
+ raise ValueError("For continuous properties, `colors` must be a list of colors representing a range")
604
+
605
+ num_colors = len(colors)
606
+ colors = {
607
+ rel_to_attr(rel): colors[round(normalized_map[rel.id] * (num_colors - 1))]
608
+ for rel in self.relationships
609
+ if rel_to_attr(rel) is not None
610
+ }
611
+
612
+ if isinstance(colors, dict):
613
+ self._color_items_dict(self.relationships, colors, override, rel_to_attr)
614
+ else:
615
+ self._color_items_iter(self.relationships, attribute, colors, override, rel_to_attr)
616
+
617
+ def _color_items_dict(
618
+ self,
619
+ items: list[Node] | list[Relationship],
620
+ colors: dict[Hashable, ColorType],
621
+ override: bool,
622
+ item_to_attr: Callable[[Any], Any],
623
+ ) -> None:
624
+ for item in items:
625
+ color = colors.get(item_to_attr(item))
468
626
 
469
627
  if color is None:
470
628
  continue
471
629
 
472
- if node.color is not None and not override:
630
+ if item.color is not None and not override:
473
631
  continue
474
632
 
475
633
  if not isinstance(color, Color):
476
- node.color = Color(color)
634
+ item.color = Color(color)
477
635
  else:
478
- node.color = color
636
+ item.color = color
479
637
 
480
- def _color_nodes_iter(
481
- self, attribute: str, colors: Iterable[ColorType], override: bool, node_to_attr: Callable[[Node], Any]
638
+ def _color_items_iter(
639
+ self,
640
+ items: list[Node] | list[Relationship],
641
+ attribute: str,
642
+ colors: Iterable[ColorType],
643
+ override: bool,
644
+ item_to_attr: Callable[[Any], Any],
482
645
  ) -> None:
483
646
  exhausted_colors = False
484
647
  prop_to_color = {}
485
648
  colors_iter = iter(colors)
486
- for node in self.nodes:
487
- raw_prop = node_to_attr(node)
649
+ for item in items:
650
+ raw_prop = item_to_attr(item)
488
651
  try:
489
652
  prop = self._make_hashable(raw_prop)
490
653
  except ValueError:
491
- raise ValueError(f"Unable to color nodes by unhashable property type '{type(raw_prop)}'")
654
+ item_type = "nodes" if isinstance(item, Node) else "relationships"
655
+ raise ValueError(f"Unable to color {item_type} by unhashable property type '{type(raw_prop)}'")
492
656
 
493
657
  if prop not in prop_to_color:
494
658
  next_color = next(colors_iter, None)
@@ -500,13 +664,13 @@ class VisualizationGraph:
500
664
 
501
665
  color = prop_to_color[prop]
502
666
 
503
- if node.color is not None and not override:
667
+ if item.color is not None and not override:
504
668
  continue
505
669
 
506
670
  if not isinstance(color, Color):
507
- node.color = Color(color)
671
+ item.color = Color(color)
508
672
  else:
509
- node.color = color
673
+ item.color = color
510
674
 
511
675
  if exhausted_colors:
512
676
  warnings.warn(
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neo4j-viz
3
- Version: 0.7.0
3
+ Version: 1.1.0
4
4
  Summary: A simple graph visualization tool
5
5
  Author-email: Neo4j <team-gds@neo4j.org>
6
6
  License-Expression: GPL-3.0-only
7
7
  Project-URL: Homepage, https://neo4j.com/
8
8
  Project-URL: Repository, https://github.com/neo4j/python-graph-visualization
9
9
  Project-URL: Issues, https://github.com/neo4j/python-graph-visualization/issues
10
- Project-URL: Documentation, https://neo4j.com/docs/nvl-python/preview
10
+ Project-URL: Documentation, https://neo4j.com/docs/python-graph-visualization/
11
11
  Keywords: graph,visualization,neo4j
12
- Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Intended Audience :: Science/Research
15
15
  Classifier: Operating System :: OS Independent
@@ -32,22 +32,6 @@ Requires-Dist: ipython<10,>=7
32
32
  Requires-Dist: pydantic<3,>=2
33
33
  Requires-Dist: pydantic-extra-types<3,>=2
34
34
  Requires-Dist: enum-tools==0.13.0
35
- Provides-Extra: dev
36
- Requires-Dist: ruff==0.14.1; extra == "dev"
37
- Requires-Dist: mypy==1.18.2; extra == "dev"
38
- Requires-Dist: pytest==8.4.2; extra == "dev"
39
- Requires-Dist: selenium==4.32.0; extra == "dev"
40
- Requires-Dist: ipykernel==6.30.1; extra == "dev"
41
- Requires-Dist: palettable==3.3.3; extra == "dev"
42
- Requires-Dist: pytest-mock==3.14.0; extra == "dev"
43
- Requires-Dist: nbconvert==7.16.6; extra == "dev"
44
- Requires-Dist: streamlit==1.45.0; extra == "dev"
45
- Requires-Dist: matplotlib>=3.9.4; extra == "dev"
46
- Provides-Extra: docs
47
- Requires-Dist: sphinx==8.1.3; extra == "docs"
48
- Requires-Dist: enum-tools[sphinx]; extra == "docs"
49
- Requires-Dist: nbsphinx==0.9.7; extra == "docs"
50
- Requires-Dist: nbsphinx-link==1.3.1; extra == "docs"
51
35
  Provides-Extra: pandas
52
36
  Requires-Dist: pandas<3,>=2; extra == "pandas"
53
37
  Requires-Dist: pandas-stubs<3,>=2; extra == "pandas"
@@ -57,14 +41,6 @@ Provides-Extra: neo4j
57
41
  Requires-Dist: neo4j; extra == "neo4j"
58
42
  Provides-Extra: snowflake
59
43
  Requires-Dist: snowflake-snowpark-python<2,>=1; extra == "snowflake"
60
- Provides-Extra: notebook
61
- Requires-Dist: ipykernel>=6.29.5; extra == "notebook"
62
- Requires-Dist: pykernel>=0.1.6; extra == "notebook"
63
- Requires-Dist: neo4j>=5.26.0; extra == "notebook"
64
- Requires-Dist: ipywidgets>=8.0.0; extra == "notebook"
65
- Requires-Dist: palettable>=3.3.3; extra == "notebook"
66
- Requires-Dist: matplotlib>=3.9.4; extra == "notebook"
67
- Requires-Dist: snowflake-snowpark-python==1.37.0; extra == "notebook"
68
44
  Dynamic: license-file
69
45
 
70
46
  # Graph Visualization for Python by Neo4j
@@ -84,9 +60,6 @@ Alternatively, you can export the output to a file and view it in a web browser.
84
60
 
85
61
  The package wraps the [Neo4j Visualization JavaScript library (NVL)](https://neo4j.com/docs/nvl/current/).
86
62
 
87
- > [!WARNING]
88
- > This package is still in development and the API is subject to change.
89
-
90
63
 
91
64
  ![Example Graph](https://github.com/neo4j/python-graph-visualization/blob/main/examples/example_cora_graph.png)
92
65
 
@@ -7,21 +7,21 @@ neo4j_viz/node.py,sha256=qXjPzsNLksY7yh_Lxe174lOQw0QjHZ9-ka9R9uJpG_A,3843
7
7
  neo4j_viz/node_size.py,sha256=c_sMtQSD8eJ_6Y0Kr6ku0LOs9VoEDxfYCUUzUWZ-1Xo,1197
8
8
  neo4j_viz/nvl.py,sha256=ZN3tyWar9ugR88r5N6txW3ThfNEWOt5A1KzrrRnLKwk,5262
9
9
  neo4j_viz/options.py,sha256=oai-yI03WxWyl6-9cFWEbQkqpXAcI8oG4G6rSVF1Bt0,6495
10
- neo4j_viz/pandas.py,sha256=gFQW9SlWxiSrVCi2kHGUKpDXDhYtlFkk2AR2DzxYTWE,4759
10
+ neo4j_viz/pandas.py,sha256=Y-td7ZyorXtfRGCiROsjApzFjxkA0DqZvJ0KEWw9Sdw,4921
11
11
  neo4j_viz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- neo4j_viz/relationship.py,sha256=aVj8_6umHSm1rg53CU-oci21W845J91AFKv1AJudmD8,4112
12
+ neo4j_viz/relationship.py,sha256=CC2Yycr1LPJh8bZNpAGiQ9wVh7hHpRcMrvpixbPvCu4,4248
13
13
  neo4j_viz/snowflake.py,sha256=Md3bW6qaBVnq8A_2fx8POkdbpKQuPc1qNaNzwDvQ8CE,12683
14
- neo4j_viz/visualization_graph.py,sha256=G4MHAQ_sgVMDqf3gI76l4m8tnFzfW0hM_lrxWFWojVc,19490
14
+ neo4j_viz/visualization_graph.py,sha256=LjhFhyp_zmupbTbWFpQAprKUE68YNjh4Xu0iQL8YRgY,26405
15
15
  neo4j_viz/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  neo4j_viz/resources/icons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  neo4j_viz/resources/icons/screenshot.svg,sha256=Ns9Yi2Iq4lIaiFvzc0pXBmjxt4fcmBO-I4cI8Xiu1HE,311
18
18
  neo4j_viz/resources/icons/zoom-in.svg,sha256=PsO5yFkA1JnGM2QV_qxHKG13qmoR-RrlWARpaXNp5qU,415
19
19
  neo4j_viz/resources/icons/zoom-out.svg,sha256=OQRADAoe2bxbCeFufg6W22nR41q5NlI8QspT9l5pXUw,400
20
20
  neo4j_viz/resources/nvl_entrypoint/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- neo4j_viz/resources/nvl_entrypoint/base.js,sha256=2Z_ofAXO1TFONMqYXjBjPd6l4U3an-h8ZNwgoMiKuPI,1820251
21
+ neo4j_viz/resources/nvl_entrypoint/base.js,sha256=UtqPbkh9qhXpt05yz0DvoaCpEMifZaUCy9cHsTUsKjI,1889766
22
22
  neo4j_viz/resources/nvl_entrypoint/styles.css,sha256=sRA-0i4Bc-P2S3FHg8FjTBiChr0ikC8f0UOYduXAXT0,1354
23
- neo4j_viz-0.7.0.dist-info/licenses/LICENSE,sha256=tWSeLI0mRp2u8Pnumaxq51JltfXLVSXl2w_D6kxhRMI,36006
24
- neo4j_viz-0.7.0.dist-info/METADATA,sha256=JW_fVOFBIyLGU5ooY02mwpN8_vSUwHr3UhlFLIKdVVw,7120
25
- neo4j_viz-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
- neo4j_viz-0.7.0.dist-info/top_level.txt,sha256=jPUM3z8MOtxqDanc2VzqkxG4HJn8aaq4S7rnCFNk_Vs,10
27
- neo4j_viz-0.7.0.dist-info/RECORD,,
23
+ neo4j_viz-1.1.0.dist-info/licenses/LICENSE,sha256=tWSeLI0mRp2u8Pnumaxq51JltfXLVSXl2w_D6kxhRMI,36006
24
+ neo4j_viz-1.1.0.dist-info/METADATA,sha256=hTcIDJoQfykzPrpGyHVJEchrUv2ZfiXSW8HVoVstWL0,5925
25
+ neo4j_viz-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
26
+ neo4j_viz-1.1.0.dist-info/top_level.txt,sha256=jPUM3z8MOtxqDanc2VzqkxG4HJn8aaq4S7rnCFNk_Vs,10
27
+ neo4j_viz-1.1.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5