Skip to content

Move the GraphVisualization wrapper#275

Open
OskarDamkjaer wants to merge 44 commits intoneo4j:mainfrom
OskarDamkjaer:react-viz
Open

Move the GraphVisualization wrapper#275
OskarDamkjaer wants to merge 44 commits intoneo4j:mainfrom
OskarDamkjaer:react-viz

Conversation

@OskarDamkjaer
Copy link

@OskarDamkjaer OskarDamkjaer commented Feb 12, 2026

I found that using anywidget makes the interaction with React /javascript much cleaner and less hacky. To avoid breaking the public API, render is still there return  creating HTML, which i implemented by creating a tiny anywidget wrapper in plain html

I also added a new render_widget is there as a new alternative for 2 way data binding with the visualisation, as well as a GraphWidget export.

The PR is quite large 😅 and I've annotated best I can where I'm uncertain.

There's some open questions that'd be good to discuss:

  • The default anywidget builds just use one folder (instead python-wrapper & js-applet) It seem cleaner & simpler to me, but it was a bigger change than what I wanted to do without discussing it first
  • It also uses tooling to rebuild the js parts, to avoid commiting the built assets. Are we interested in doing that?
  • How do we do with documentation of the new widget? Could it even make sense to do a major version and break the API? Or maybe we leave it as a separate entrypoint until it's "proven itself"?
  • I'd love to get some reality checks some of the changes to python/integrations with other data sources. Some guidance/chats on how I best test manually/automatically would be good
  • Is there a good way I can test out how this change would work in production/in a deployed mode, so I can verify I don't make unintended breaking changes?
  • I don't see the integration tests running on PR, why not?

"scripts": {
"build": "webpack",
"postbuild": "cp dist/base.js ../python-wrapper/src/neo4j_viz/resources/nvl_entrypoint"
"dev": "concurrently -n widget,html,jupyter -c cyan,magenta,green \"npm:dev:widget\" \"npm:dev:html\" \"npm:dev:jupyter\"",
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convenience script to make developing faster/easier with hot reloading (no need to reload the kernel to see changes in javascript)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice 👍

could mention this in the Contributing.md

@OskarDamkjaer OskarDamkjaer marked this pull request as ready for review February 12, 2026 15:35
@OskarDamkjaer OskarDamkjaer requested a review from a team as a code owner February 12, 2026 15:35
@OskarDamkjaer OskarDamkjaer changed the title [wip] Move the GraphVisualization wrapper Move the GraphVisualization wrapper Feb 12, 2026
Copy link
Collaborator

@FlorentinD FlorentinD left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your PR!

I think we can reduce the breaking changes a bit.
Couple of suggestions and questions. For the python parts, I can also try to apply some of them.

I would also put a link to https://storybook-components-build.appspot.com/?path=/story/components-graph-visualization--default for reference in the graph-widget.tsx file.

I think before merging the PR, we should change:

[ ] default coloring changed (fixable by setting the color in python if not set initially)
[ ] Zoom in/out button missing (fixable)

On the zoom in/out, i dont get yet why the storybook example has the buttons but its not referenced in the code

// Only include visual properties when explicitly set, so that
// GraphVisualization's smart defaults (label-based coloring, etc.) apply.
...(node.color !== undefined && { color: node.color }),
...(node.size !== undefined && { size: node.size }),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there docs around which are the smart defaults?

id: node.id,
// Only include visual properties when explicitly set, so that
// GraphVisualization's smart defaults (label-based coloring, etc.) apply.
...(node.color !== undefined && { color: node.color }),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compared to before - how can users now select one color for all nodes.
They could fix the color for each node

...(node.color !== undefined && { color: node.color }),
...(node.size !== undefined && { size: node.size }),
...(node.pinned !== undefined && { pinned: node.pinned }),
labels: Array.isArray(labels)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are the labels used for the results overview?

asking for when nodes/relationships are colored differently, such as by size or a property.
right now after VG.color_nodes(field="size"), the overview still shows the coloring based on the labels.

this is okay for now, we didnt have any overview before.
But for the future, maybe we could provide a hint for the colors in the overview?

_STATIC = pathlib.Path(__file__).parent / "resources" / "nvl_entrypoint"


class GraphWidget(anywidget.AnyWidget):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like the options this will give us.
might want to allow some utility methods in the future directly on the widget :)

Comment on lines +53 to +54
nodes: list[dict[str, Any]] = traitlets.List([]).tag(sync=True) # type: ignore[assignment]
relationships: list[dict[str, Any]] = traitlets.List([]).tag(sync=True) # type: ignore[assignment]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about we use typed Node and relationship here, but specify a custom to_json method?

example from Anywidget

    my_path = traitlets.Instance(pathlib.Path).tag(
        sync=True, to_json=_serialize_entity
    )

Comment on lines +105 to +108
# Simulate adding a node (as JS or Python might do)
widget.nodes = [*widget.nodes, {"id": "n2", "caption": "B"}]
assert len(widget.nodes) == 2
assert widget.nodes[1]["id"] == "n2"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats quite neat.
would make a good addition for an example notebook

Co-authored-by: Florentin Dörre <florentin@owitsch.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants