From 3983943522e9248f8729a558570d2d9deb1ad4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Fri, 6 Jun 2025 17:41:23 +0200 Subject: [PATCH 1/6] Expose layout options --- changelog.md | 2 + docs/source/rendering.rst | 4 +- python-wrapper/src/neo4j_viz/__init__.py | 23 +++++++- python-wrapper/src/neo4j_viz/options.py | 58 ++++++++++++++++++- .../src/neo4j_viz/visualization_graph.py | 6 +- python-wrapper/tests/test_options.py | 17 +++++- 6 files changed, 100 insertions(+), 10 deletions(-) diff --git a/changelog.md b/changelog.md index 37b975a8..774571de 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,8 @@ * Allow visualization based only on relationship DataFrames, without specifying node DataFrames in `from_dfs` * Add relationship properties to `VisualizationGraph` when constructing via `from_gds`. +* Allow setting `layout_options` for `VisualizationGraph::render` + ## Bug fixes diff --git a/docs/source/rendering.rst b/docs/source/rendering.rst index 8d2d0792..a2b4e91f 100644 --- a/docs/source/rendering.rst +++ b/docs/source/rendering.rst @@ -35,8 +35,8 @@ You can provide these either as a percentage of the available space (eg. ``"80%" Further you can change the layout of the graph using the ``layout`` parameter, which can be set to one of the following values: -* ``Layout.FORCE_DIRECTED`` - Nodes are arranged using the Force-Directed algorithm, which simulates physical forces -* ``Layout.HIERARCHICAL`` - Arranges nodes by the directionality of their relationships, creating a tree-like structure +* ``Layout.FORCE_DIRECTED`` - Nodes are arranged using the Force-Directed algorithm, which simulates physical forces. To customize the layout use `ForceDirectedOptions` via `layout_options`.` +* ``Layout.HIERARCHICAL`` - Arranges nodes by the directionality of their relationships, creating a tree-like structure. To customize the layout use `HierarchicalLayoutOptions` via `layout_options`.` * ``Layout.COORDINATE`` - Arranges nodes based on coordinates defined in `x` and `y` properties on each node. Another thing of note is the ``max_allowed_nodes`` parameter, which controls the maximum number of nodes that is allowed diff --git a/python-wrapper/src/neo4j_viz/__init__.py b/python-wrapper/src/neo4j_viz/__init__.py index 4f7be89b..ab7ea34a 100644 --- a/python-wrapper/src/neo4j_viz/__init__.py +++ b/python-wrapper/src/neo4j_viz/__init__.py @@ -1,6 +1,25 @@ from .node import Node -from .options import CaptionAlignment, Layout, Renderer +from .options import ( + CaptionAlignment, + Layout, + Renderer, + HierarchicalLayoutOptions, + ForceDirectedLayoutOptions, + Direction, + Packing, +) from .relationship import Relationship from .visualization_graph import VisualizationGraph -__all__ = ["VisualizationGraph", "Node", "Relationship", "CaptionAlignment", "Layout", "Renderer"] +__all__ = [ + "VisualizationGraph", + "Node", + "Relationship", + "CaptionAlignment", + "Layout", + "Renderer", + "HierarchicalLayoutOptions", + "ForceDirectedLayoutOptions", + "Direction", + "Packing", +] diff --git a/python-wrapper/src/neo4j_viz/options.py b/python-wrapper/src/neo4j_viz/options.py index 0a17b44c..51fcd6d6 100644 --- a/python-wrapper/src/neo4j_viz/options.py +++ b/python-wrapper/src/neo4j_viz/options.py @@ -2,10 +2,10 @@ import warnings from enum import Enum -from typing import Any, Optional +from typing import Any, Optional, Union import enum_tools.documentation -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator @enum_tools.documentation.document_enum @@ -33,6 +33,49 @@ class Layout(str, Enum): GRID = "grid" +@enum_tools.documentation.document_enum +class Direction(str, Enum): + """ + The direction in which the layout should be oriented + """ + + LEFT = "left" + RIGHT = "right" + UP = "up" + DOWN = "down" + + +@enum_tools.documentation.document_enum +class Packing(str, Enum): + """ + The packing method to be used + """ + + BIN = "bin" + STACK = "stack" + + +class HierarchicalLayoutOptions(BaseModel): + """ + The options for the hierarchical layout. + """ + + direction: Optional[Direction] = None + packaging: Optional[Packing] = None + + +class ForceDirectedLayoutOptions(BaseModel): + """ + The options for the force-directed layout. + """ + + gravity: Optional[float] = None + simulationStopVelocity: Optional[float] = None + + +LayoutOptions = Union[HierarchicalLayoutOptions, ForceDirectedLayoutOptions] + + @enum_tools.documentation.document_enum class Renderer(str, Enum): """ @@ -72,6 +115,9 @@ class RenderOptions(BaseModel, extra="allow"): """ layout: Optional[Layout] = None + layout_options: Optional[Union[HierarchicalLayoutOptions, ForceDirectedLayoutOptions]] = Field( + None, serialization_alias="layoutOptions" + ) renderer: Optional[Renderer] = None pan_X: Optional[float] = Field(None, serialization_alias="panX") @@ -84,5 +130,13 @@ class RenderOptions(BaseModel, extra="allow"): min_zoom: Optional[float] = Field(None, serialization_alias="minZoom", description="The minimum zoom level allowed") allow_dynamic_min_zoom: Optional[bool] = Field(None, serialization_alias="allowDynamicMinZoom") + @model_validator(mode="after") + def check_layout_options_match(self) -> RenderOptions: + if self.layout == Layout.HIERARCHICAL and not isinstance(self.layout_options, HierarchicalLayoutOptions): + raise ValueError("layout_options must be of type HierarchicalLayoutOptions for hierarchical layout") + if self.layout == Layout.FORCE_DIRECTED and not isinstance(self.layout_options, ForceDirectedLayoutOptions): + raise ValueError("layout_options must be of type ForceDirectedLayoutOptions for force-directed layout") + return self + def to_dict(self) -> dict[str, Any]: return self.model_dump(exclude_none=True, by_alias=True) diff --git a/python-wrapper/src/neo4j_viz/visualization_graph.py b/python-wrapper/src/neo4j_viz/visualization_graph.py index c57d2d88..52aee9a9 100644 --- a/python-wrapper/src/neo4j_viz/visualization_graph.py +++ b/python-wrapper/src/neo4j_viz/visualization_graph.py @@ -11,7 +11,7 @@ from .node import Node, NodeIdType from .node_size import RealNumber, verify_radii from .nvl import NVL -from .options import Layout, Renderer, RenderOptions +from .options import Layout, LayoutOptions, Renderer, RenderOptions from .relationship import Relationship @@ -42,6 +42,7 @@ def __init__(self, nodes: list[Node], relationships: list[Relationship]) -> None def render( self, layout: Optional[Layout] = None, + layout_options: Optional[LayoutOptions] = None, renderer: Renderer = Renderer.CANVAS, width: str = "100%", height: str = "600px", @@ -60,6 +61,8 @@ def render( ---------- layout: The `Layout` to use. + layout_options: + The `LayoutOptions` to use. renderer: The `Renderer` to use. width: @@ -94,6 +97,7 @@ def render( render_options = RenderOptions( layout=layout, + layout_options=layout_options, renderer=renderer, pan_X=pan_position[0] if pan_position is not None else None, pan_Y=pan_position[1] if pan_position is not None else None, diff --git a/python-wrapper/tests/test_options.py b/python-wrapper/tests/test_options.py index 2f34b480..8ea0bfa5 100644 --- a/python-wrapper/tests/test_options.py +++ b/python-wrapper/tests/test_options.py @@ -1,7 +1,18 @@ -from neo4j_viz.options import Layout, RenderOptions +import pytest + +from neo4j_viz.options import Direction, HierarchicalLayoutOptions, Layout, RenderOptions def test_render_options() -> None: - options = RenderOptions(layout=Layout.FORCE_DIRECTED) + options = RenderOptions( + layout=Layout.HIERARCHICAL, layout_options=HierarchicalLayoutOptions(direction=Direction.LEFT) + ) + + assert options.to_dict() == {"layout": "hierarchical", "layoutOptions": {"direction": "left"}} + - assert options.to_dict() == {"layout": "forcedirected"} +def test_layout_options_match() -> None: + with pytest.raises( + ValueError, match="layout_options must be of type ForceDirectedLayoutOptions for force-directed layout" + ): + RenderOptions(layout=Layout.FORCE_DIRECTED, layout_options=HierarchicalLayoutOptions(direction=Direction.LEFT)) From 80a3ab3713e6757be2290969814cd16e8fb2ee36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Tue, 10 Jun 2025 09:49:19 +0200 Subject: [PATCH 2/6] Improve imports --- python-wrapper/src/neo4j_viz/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python-wrapper/src/neo4j_viz/__init__.py b/python-wrapper/src/neo4j_viz/__init__.py index ab7ea34a..9db951c9 100644 --- a/python-wrapper/src/neo4j_viz/__init__.py +++ b/python-wrapper/src/neo4j_viz/__init__.py @@ -1,12 +1,12 @@ from .node import Node from .options import ( CaptionAlignment, - Layout, - Renderer, - HierarchicalLayoutOptions, - ForceDirectedLayoutOptions, Direction, + ForceDirectedLayoutOptions, + HierarchicalLayoutOptions, + Layout, Packing, + Renderer, ) from .relationship import Relationship from .visualization_graph import VisualizationGraph From 2fae1f2ef7b79d291b8acfd0f8334e50d1a194ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Tue, 10 Jun 2025 12:22:36 +0200 Subject: [PATCH 3/6] Allow layout options to be empty --- python-wrapper/src/neo4j_viz/options.py | 3 +++ python-wrapper/tests/test_options.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/python-wrapper/src/neo4j_viz/options.py b/python-wrapper/src/neo4j_viz/options.py index 51fcd6d6..99aa1d50 100644 --- a/python-wrapper/src/neo4j_viz/options.py +++ b/python-wrapper/src/neo4j_viz/options.py @@ -132,6 +132,9 @@ class RenderOptions(BaseModel, extra="allow"): @model_validator(mode="after") def check_layout_options_match(self) -> RenderOptions: + if self.layout_options is None: + return self + if self.layout == Layout.HIERARCHICAL and not isinstance(self.layout_options, HierarchicalLayoutOptions): raise ValueError("layout_options must be of type HierarchicalLayoutOptions for hierarchical layout") if self.layout == Layout.FORCE_DIRECTED and not isinstance(self.layout_options, ForceDirectedLayoutOptions): diff --git a/python-wrapper/tests/test_options.py b/python-wrapper/tests/test_options.py index 8ea0bfa5..47306d1d 100644 --- a/python-wrapper/tests/test_options.py +++ b/python-wrapper/tests/test_options.py @@ -4,6 +4,12 @@ def test_render_options() -> None: + options = RenderOptions(layout=Layout.HIERARCHICAL) + + assert options.to_dict() == {"layout": "hierarchical"} + + +def test_render_options_with_layout_options() -> None: options = RenderOptions( layout=Layout.HIERARCHICAL, layout_options=HierarchicalLayoutOptions(direction=Direction.LEFT) ) From 8328007972c98dbfc252ddac2560e01012ff1cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Fri, 13 Jun 2025 11:43:25 +0200 Subject: [PATCH 4/6] Allow user to pass untyped layout options typing is done by us internally + validation Co-authored-by: Adam Schill Collberg --- python-wrapper/src/neo4j_viz/__init__.py | 4 -- python-wrapper/src/neo4j_viz/options.py | 44 +++++++++++++++++-- .../src/neo4j_viz/visualization_graph.py | 18 ++++++-- python-wrapper/tests/test_render.py | 22 ++++++++++ 4 files changed, 78 insertions(+), 10 deletions(-) diff --git a/python-wrapper/src/neo4j_viz/__init__.py b/python-wrapper/src/neo4j_viz/__init__.py index 9db951c9..e58b3c89 100644 --- a/python-wrapper/src/neo4j_viz/__init__.py +++ b/python-wrapper/src/neo4j_viz/__init__.py @@ -2,8 +2,6 @@ from .options import ( CaptionAlignment, Direction, - ForceDirectedLayoutOptions, - HierarchicalLayoutOptions, Layout, Packing, Renderer, @@ -18,8 +16,6 @@ "CaptionAlignment", "Layout", "Renderer", - "HierarchicalLayoutOptions", - "ForceDirectedLayoutOptions", "Direction", "Packing", ] diff --git a/python-wrapper/src/neo4j_viz/options.py b/python-wrapper/src/neo4j_viz/options.py index 99aa1d50..cb67bc62 100644 --- a/python-wrapper/src/neo4j_viz/options.py +++ b/python-wrapper/src/neo4j_viz/options.py @@ -5,7 +5,7 @@ from typing import Any, Optional, Union import enum_tools.documentation -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, ValidationError, model_validator @enum_tools.documentation.document_enum @@ -55,7 +55,7 @@ class Packing(str, Enum): STACK = "stack" -class HierarchicalLayoutOptions(BaseModel): +class HierarchicalLayoutOptions(BaseModel, extra="forbid"): """ The options for the hierarchical layout. """ @@ -64,7 +64,7 @@ class HierarchicalLayoutOptions(BaseModel): packaging: Optional[Packing] = None -class ForceDirectedLayoutOptions(BaseModel): +class ForceDirectedLayoutOptions(BaseModel, extra="forbid"): """ The options for the force-directed layout. """ @@ -76,6 +76,26 @@ class ForceDirectedLayoutOptions(BaseModel): LayoutOptions = Union[HierarchicalLayoutOptions, ForceDirectedLayoutOptions] +def construct_layout_options(layout: Layout, options: dict[str, Any]) -> Optional[LayoutOptions]: + if not options: + return None + + if layout == Layout.FORCE_DIRECTED: + try: + return ForceDirectedLayoutOptions(**options) + except ValidationError as e: + _parse_validation_error(e, ForceDirectedLayoutOptions) + elif layout == Layout.HIERARCHICAL: + try: + return HierarchicalLayoutOptions(**options) + except ValidationError as e: + _parse_validation_error(e, ForceDirectedLayoutOptions) + + raise ValueError( + f"Layout options only supported for layouts `{Layout.FORCE_DIRECTED}` and `{Layout.HIERARCHICAL}`, but was `{layout}`" + ) + + @enum_tools.documentation.document_enum class Renderer(str, Enum): """ @@ -143,3 +163,21 @@ def check_layout_options_match(self) -> RenderOptions: def to_dict(self) -> dict[str, Any]: return self.model_dump(exclude_none=True, by_alias=True) + + +def _parse_validation_error(e: ValidationError, entity_type: type[BaseModel]) -> None: + for err in e.errors(): + loc = err["loc"][0] + if err["type"] == "missing": + raise ValueError( + f"Mandatory `{entity_type.__name__}` parameter '{loc}' is missing. Expected one of {entity_type.model_fields[loc].validation_alias.choices} to be present" # type: ignore + ) + elif err["type"] == "extra_forbidden": + raise ValueError( + f"Unexpected `{entity_type.__name__}` parameter '{loc}' with provided input '{err['input']}'. " + f"Allowed parameters are: {', '.join(entity_type.model_fields.keys())}" + ) + else: + raise ValueError( + f"Error for `{entity_type.__name__}` parameter '{loc}' with provided input '{err['input']}'. Reason: {err['msg']}" + ) diff --git a/python-wrapper/src/neo4j_viz/visualization_graph.py b/python-wrapper/src/neo4j_viz/visualization_graph.py index 52aee9a9..98597158 100644 --- a/python-wrapper/src/neo4j_viz/visualization_graph.py +++ b/python-wrapper/src/neo4j_viz/visualization_graph.py @@ -11,7 +11,12 @@ from .node import Node, NodeIdType from .node_size import RealNumber, verify_radii from .nvl import NVL -from .options import Layout, LayoutOptions, Renderer, RenderOptions +from .options import ( + Layout, + Renderer, + RenderOptions, + construct_layout_options, +) from .relationship import Relationship @@ -42,7 +47,7 @@ def __init__(self, nodes: list[Node], relationships: list[Relationship]) -> None def render( self, layout: Optional[Layout] = None, - layout_options: Optional[LayoutOptions] = None, + layout_options: Optional[dict[str, Any]] = None, renderer: Renderer = Renderer.CANVAS, width: str = "100%", height: str = "600px", @@ -95,9 +100,16 @@ def render( Renderer.check(renderer, num_nodes) + if not layout: + layout = Layout.FORCE_DIRECTED + if not layout_options: + layout_options = {} + + layout_options_typed = construct_layout_options(layout, layout_options) + render_options = RenderOptions( layout=layout, - layout_options=layout_options, + layout_options=layout_options_typed, renderer=renderer, pan_X=pan_position[0] if pan_position is not None else None, pan_Y=pan_position[1] if pan_position is not None else None, diff --git a/python-wrapper/tests/test_render.py b/python-wrapper/tests/test_render.py index b04d4b9b..948f64f2 100644 --- a/python-wrapper/tests/test_render.py +++ b/python-wrapper/tests/test_render.py @@ -14,6 +14,8 @@ "force layout": {"layout": Layout.FORCE_DIRECTED}, "grid layout": {"layout": Layout.GRID}, "coordinate layout": {"layout": Layout.COORDINATE}, + "hierarchical layout + options": {"layout": Layout.HIERARCHICAL, "layout_options": {"direction": "left"}}, + "with layout options": {"layout_options": {"gravity": 0.1}}, } @@ -111,3 +113,23 @@ def test_render_non_json_serializable() -> None: VG = VisualizationGraph(nodes=[node], relationships=[]) # Should not raise an error VG.render() + + +def test_render_with_wrong_layout_options() -> None: + nodes = [ + Node(id="4:d09f48a4-5fca-421d-921d-a30a896c604d:0", caption="Person", x=1, y=10), + ] + + VG = VisualizationGraph(nodes=nodes, relationships=[]) + + with pytest.raises( + ValueError, + match="Unexpected `ForceDirectedLayoutOptions` parameter 'direction' with provided input 'left'", + ): + VG.render(layout_options={"direction": "left"}) + + with pytest.raises( + ValueError, + match="Unexpected `ForceDirectedLayoutOptions` parameter 'direction' with provided input 'left'", + ): + VG.render(layout=Layout.FORCE_DIRECTED, layout_options={"direction": "left"}) From e1411a500253df6392fcc321bb2726b26e39ede7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Fri, 13 Jun 2025 11:55:10 +0200 Subject: [PATCH 5/6] Add layout options types to API reference Co-authored-by: Adam Schill Collberg --- changelog.md | 2 +- docs/source/api-reference/render_options.rst | 15 +++++++++++++++ docs/source/rendering.rst | 2 +- python-wrapper/src/neo4j_viz/__init__.py | 4 ++++ .../src/neo4j_viz/visualization_graph.py | 5 +++-- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 774571de..b30a6779 100644 --- a/changelog.md +++ b/changelog.md @@ -7,7 +7,7 @@ ## New features * Allow visualization based only on relationship DataFrames, without specifying node DataFrames in `from_dfs` -* Add relationship properties to `VisualizationGraph` when constructing via `from_gds`. +* Add relationship properties to `VisualizationGraph` when constructing via `from_gds` * Allow setting `layout_options` for `VisualizationGraph::render` diff --git a/docs/source/api-reference/render_options.rst b/docs/source/api-reference/render_options.rst index a1cdf299..b477b3f6 100644 --- a/docs/source/api-reference/render_options.rst +++ b/docs/source/api-reference/render_options.rst @@ -4,5 +4,20 @@ .. autoenum:: neo4j_viz.Layout :members: +.. autoclass:: neo4j_viz.Force-Directed + :members: + :exclude-members: model_config + +.. autoclass:: neo4j_viz.HierarchicalLayoutOptions + :members: + :exclude-members: model_config + +.. autoenum:: neo4j_viz.options.Direction + :members: + +.. autoenum:: neo4j_viz.options.Packing + :members: + + .. autoenum:: neo4j_viz.Renderer :members: diff --git a/docs/source/rendering.rst b/docs/source/rendering.rst index a2b4e91f..8e648586 100644 --- a/docs/source/rendering.rst +++ b/docs/source/rendering.rst @@ -35,7 +35,7 @@ You can provide these either as a percentage of the available space (eg. ``"80%" Further you can change the layout of the graph using the ``layout`` parameter, which can be set to one of the following values: -* ``Layout.FORCE_DIRECTED`` - Nodes are arranged using the Force-Directed algorithm, which simulates physical forces. To customize the layout use `ForceDirectedOptions` via `layout_options`.` +* ``Layout.FORCE_DIRECTED`` - Nodes are arranged using the Force-Directed algorithm, which simulates physical forces. To customize the layout, use `ForceDirectedOptions` via `layout_options`.` * ``Layout.HIERARCHICAL`` - Arranges nodes by the directionality of their relationships, creating a tree-like structure. To customize the layout use `HierarchicalLayoutOptions` via `layout_options`.` * ``Layout.COORDINATE`` - Arranges nodes based on coordinates defined in `x` and `y` properties on each node. diff --git a/python-wrapper/src/neo4j_viz/__init__.py b/python-wrapper/src/neo4j_viz/__init__.py index e58b3c89..61d33c52 100644 --- a/python-wrapper/src/neo4j_viz/__init__.py +++ b/python-wrapper/src/neo4j_viz/__init__.py @@ -5,6 +5,8 @@ Layout, Packing, Renderer, + HierarchicalLayoutOptions, + ForceDirectedLayoutOptions, ) from .relationship import Relationship from .visualization_graph import VisualizationGraph @@ -16,6 +18,8 @@ "CaptionAlignment", "Layout", "Renderer", + "ForceDirectedLayoutOptions", + "HierarchicalLayoutOptions", "Direction", "Packing", ] diff --git a/python-wrapper/src/neo4j_viz/visualization_graph.py b/python-wrapper/src/neo4j_viz/visualization_graph.py index 98597158..11c5e27b 100644 --- a/python-wrapper/src/neo4j_viz/visualization_graph.py +++ b/python-wrapper/src/neo4j_viz/visualization_graph.py @@ -2,7 +2,7 @@ import warnings from collections.abc import Iterable -from typing import Any, Callable, Hashable, Optional +from typing import Any, Callable, Hashable, Optional, Union from IPython.display import HTML from pydantic_extra_types.color import Color, ColorType @@ -13,6 +13,7 @@ from .nvl import NVL from .options import ( Layout, + LayoutOptions, Renderer, RenderOptions, construct_layout_options, @@ -47,7 +48,7 @@ def __init__(self, nodes: list[Node], relationships: list[Relationship]) -> None def render( self, layout: Optional[Layout] = None, - layout_options: Optional[dict[str, Any]] = None, + layout_options: Union[dict[str, Any], LayoutOptions, None] = None, renderer: Renderer = Renderer.CANVAS, width: str = "100%", height: str = "600px", From 5d60f974abd99d550cd0e49460ef8d3d26eead8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Fri, 13 Jun 2025 11:57:17 +0200 Subject: [PATCH 6/6] Fix checkstyle --- python-wrapper/src/neo4j_viz/__init__.py | 4 ++-- python-wrapper/src/neo4j_viz/visualization_graph.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/python-wrapper/src/neo4j_viz/__init__.py b/python-wrapper/src/neo4j_viz/__init__.py index 61d33c52..1f0ef2c0 100644 --- a/python-wrapper/src/neo4j_viz/__init__.py +++ b/python-wrapper/src/neo4j_viz/__init__.py @@ -2,11 +2,11 @@ from .options import ( CaptionAlignment, Direction, + ForceDirectedLayoutOptions, + HierarchicalLayoutOptions, Layout, Packing, Renderer, - HierarchicalLayoutOptions, - ForceDirectedLayoutOptions, ) from .relationship import Relationship from .visualization_graph import VisualizationGraph diff --git a/python-wrapper/src/neo4j_viz/visualization_graph.py b/python-wrapper/src/neo4j_viz/visualization_graph.py index 11c5e27b..33a188bc 100644 --- a/python-wrapper/src/neo4j_viz/visualization_graph.py +++ b/python-wrapper/src/neo4j_viz/visualization_graph.py @@ -106,7 +106,10 @@ def render( if not layout_options: layout_options = {} - layout_options_typed = construct_layout_options(layout, layout_options) + if isinstance(layout_options, dict): + layout_options_typed = construct_layout_options(layout, layout_options) + else: + layout_options_typed = layout_options render_options = RenderOptions( layout=layout,