anywidget-graph 0.2.7__tar.gz → 0.2.9__tar.gz

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.
Files changed (45) hide show
  1. anywidget_graph-0.2.9/CHANGELOG.md +112 -0
  2. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/PKG-INFO +3 -2
  3. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/README.md +1 -0
  4. anywidget_graph-0.2.9/examples/test_large_graph.py +91 -0
  5. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/pyproject.toml +2 -2
  6. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/__init__.py +1 -1
  7. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/backends/grafeo.py +61 -3
  8. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/ui/__init__.py +4 -0
  9. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/ui/icons.js +2 -0
  10. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/ui/index.js +355 -27
  11. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/ui/properties.js +5 -4
  12. anywidget_graph-0.2.9/src/anywidget_graph/ui/results.js +183 -0
  13. anywidget_graph-0.2.9/src/anywidget_graph/ui/schema.js +452 -0
  14. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/ui/styles.css +216 -1
  15. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/ui/toolbar.js +16 -0
  16. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/widget.py +4 -0
  17. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/tests/test_graph.py +148 -0
  18. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/uv.lock +2 -2
  19. anywidget_graph-0.2.7/src/anywidget_graph/ui/schema.js +0 -239
  20. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/.github/workflows/ci.yml +0 -0
  21. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/.github/workflows/pypi.yml +0 -0
  22. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/.gitignore +0 -0
  23. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/.python-version +0 -0
  24. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/LICENSE +0 -0
  25. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/examples/demo.py +0 -0
  26. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/backends/__init__.py +0 -0
  27. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/backends/arango.py +0 -0
  28. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/backends/cosmosdb.py +0 -0
  29. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/backends/ladybug.py +0 -0
  30. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/backends/neo4j.py +0 -0
  31. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/converters/__init__.py +0 -0
  32. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/converters/base.py +0 -0
  33. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/converters/common.py +0 -0
  34. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/converters/cypher.py +0 -0
  35. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/converters/gql.py +0 -0
  36. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/converters/graphql.py +0 -0
  37. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/converters/gremlin.py +0 -0
  38. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/converters/sparql.py +0 -0
  39. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/demo.py +0 -0
  40. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/py.typed +0 -0
  41. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/ui/grafeo-embed.js +0 -0
  42. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/ui/grafeo.js +0 -0
  43. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/ui/neo4j.js +0 -0
  44. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/src/anywidget_graph/ui/settings.js +0 -0
  45. {anywidget_graph-0.2.7 → anywidget_graph-0.2.9}/tests/__init__.py +0 -0
@@ -0,0 +1,112 @@
1
+ # Changelog
2
+
3
+ ## 0.2.9
4
+
5
+ ### New Features
6
+
7
+ - **Filter panel**: Schema sidebar transformed into a filter panel with per-type checkboxes, counts, color pickers and "all / none" toggle links for both node and edge types
8
+ - **Type-based node coloring**: Nodes automatically colored by their type (first label), matching the filter panel swatches
9
+ - **Color pickers**: Click any type's color swatch in the filter panel to change it; graph updates in real-time
10
+ - **Level-of-detail labels**: Labels auto-hide on small or zoomed-out nodes; thresholds scale with graph size
11
+ - **Circle-packing cluster layout**: Deterministic circle packing with radius proportional to `sqrt(nodeCount)` and mini-force within each cluster for organic internal structure
12
+ - **Edge interaction**: Edges are now clickable with type shown in the properties panel
13
+
14
+ ### Bug Fixes
15
+
16
+ - Fixed widget height growing on scroll; wrapper now locked to fixed height
17
+ - Fixed search filtering: now shows matching nodes plus first-degree neighbors and connecting edges, hides everything else
18
+ - Fixed parallel edge crash by using Graphology multi-graph mode
19
+ - Fixed graph disappearing when side panels open by refreshing renderer on resize
20
+ - Fixed dark mode label color: adapts to current theme
21
+
22
+ ### Improvements
23
+
24
+ - Zoom controls reordered: fit, zoom-in, zoom-out at top; layout and mode at bottom
25
+ - Edge type shown in properties panel on click
26
+ - Internal attributes (`nodeType`, `edgeType`) hidden from properties panel
27
+
28
+ ## 0.2.8
29
+
30
+ ### New Features
31
+
32
+ - **Language-aware query dispatch**: GrafeoBackend routes queries to language-specific methods (`execute_cypher`, `execute_gremlin`, etc.) with fallback to generic `execute()`
33
+ - **Results drawer**: Bottom drawer with tabbed nodes/edges tables, pagination and query timing
34
+ - **Grafeo native dict support**: Detection and conversion of Grafeo's internal `_id`/`_labels`/`_source`/`_target`/`_type` dict format so native query results render correctly
35
+
36
+ ### Improvements
37
+
38
+ - Added playground link at grafeo.ai to README
39
+
40
+ ## 0.2.7
41
+
42
+ ### Bug Fixes
43
+
44
+ - Fixed lasso/box selection behavior
45
+
46
+ ### Improvements
47
+
48
+ - Quality-of-life improvements to search and toolbar
49
+
50
+ ## 0.2.6
51
+
52
+ ### New Features
53
+
54
+ - **Browser-side backends**: Added Grafeo server (HTTP), Grafeo WASM (browser-embedded) and Neo4j browser-side driver support
55
+ - **Settings panel**: Full connection UI with backend selector, connection mode, language picker and connect/disconnect
56
+ - **CosmosDB backend**: Added Azure CosmosDB Gremlin backend
57
+ - **Demo mode**: WASM-based demo with pre-populated dataset
58
+ - **Schema browser**: Sidebar with node labels and relationship types; click to query
59
+ - **Properties panel**: Right sidebar showing selected node/edge details
60
+ - **Zoom and pan controls**: Zoom in/out/fit buttons, box select mode, click select mode
61
+
62
+ ### Improvements
63
+
64
+ - Updated docs, tests and dependencies
65
+ - HTML export template fix
66
+ - Zoom controls added to graph container
67
+
68
+ ## 0.2.5
69
+
70
+ ### New Features
71
+
72
+ - **Major UI overhaul**: Modular component architecture with toolbar, schema panel, settings, properties and results drawer
73
+ - **Multiple backends**: Grafeo, Neo4j, LadybugDB, ArangoDB and CosmosDB support
74
+ - **Test suite**: Comprehensive test coverage with 55+ tests
75
+ - **Node interaction**: Click, drag, pin, double-click expand, delete and multi-select
76
+ - **Force layout**: ForceAtlas2 with dynamic parameters scaling by graph size
77
+ - **Keyboard shortcuts**: F to fit, Escape to deselect, Delete to remove selected
78
+
79
+ ### Improvements
80
+
81
+ - Consistent code formatting with ruff
82
+
83
+ ## 0.2.4
84
+
85
+ ### Bug Fixes
86
+
87
+ - Fixed ESM bundling issue in UI init
88
+
89
+ ## 0.2.3
90
+
91
+ ### Bug Fixes
92
+
93
+ - Fixed rendering issue in graph container
94
+ - Updated dependencies
95
+
96
+ ## 0.2.2
97
+
98
+ ### Improvements
99
+
100
+ - Added Apache 2.0 license
101
+ - ESM bundle aggregation for widget JS
102
+
103
+ ## 0.2.1
104
+
105
+ ### Improvements
106
+
107
+ - Updated README with usage examples
108
+ - Code formatting pass
109
+
110
+ ## 0.1.0
111
+
112
+ - Initial release of anywidget-graph
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anywidget-graph
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: Interactive graph visualization for Python notebooks using anywidget
5
5
  Project-URL: Homepage, https://grafeo.dev/
6
6
  Project-URL: Repository, https://github.com/GrafeoDB/anywidget-graph
@@ -29,7 +29,7 @@ Requires-Dist: marimo>=0.20; extra == 'dev'
29
29
  Requires-Dist: prek>=0.3; extra == 'dev'
30
30
  Requires-Dist: pytest>=9.0.2; extra == 'dev'
31
31
  Requires-Dist: ruff>=0.15; extra == 'dev'
32
- Requires-Dist: ty>=0.0.20; extra == 'dev'
32
+ Requires-Dist: ty>=0.0; extra == 'dev'
33
33
  Provides-Extra: embed
34
34
  Provides-Extra: grafeo
35
35
  Requires-Dist: grafeo>=0.5; extra == 'grafeo'
@@ -326,6 +326,7 @@ graph.save_html("graph.html") # Write HTML to file
326
326
  - [anywidget](https://anywidget.dev/), custom Jupyter widgets made easy
327
327
  - [Grafeo](https://github.com/GrafeoDB/grafeo), embeddable graph database
328
328
  - [grafeo-web](https://github.com/GrafeoDB/grafeo-web), Grafeo in the browser
329
+ - [Playground](https://grafeo.ai), interactive browser playground for Grafeo
329
330
 
330
331
  ## License
331
332
 
@@ -285,6 +285,7 @@ graph.save_html("graph.html") # Write HTML to file
285
285
  - [anywidget](https://anywidget.dev/), custom Jupyter widgets made easy
286
286
  - [Grafeo](https://github.com/GrafeoDB/grafeo), embeddable graph database
287
287
  - [grafeo-web](https://github.com/GrafeoDB/grafeo-web), Grafeo in the browser
288
+ - [Playground](https://grafeo.ai), interactive browser playground for Grafeo
288
289
 
289
290
  ## License
290
291
 
@@ -0,0 +1,91 @@
1
+ """Marimo demo: large graph to test filtering, search, and LOD."""
2
+
3
+ import marimo
4
+
5
+ __generated_with = "0.19.5"
6
+ app = marimo.App(width="medium")
7
+
8
+
9
+ @app.cell
10
+ def _():
11
+ import marimo as mo
12
+
13
+ return (mo,)
14
+
15
+
16
+ @app.cell
17
+ def _():
18
+ from anywidget_graph import Graph
19
+
20
+ return (Graph,)
21
+
22
+
23
+ @app.cell
24
+ def _(Graph):
25
+ import random
26
+
27
+ random.seed(42)
28
+
29
+ node_types = {
30
+ "Person": 40,
31
+ "Company": 15,
32
+ "Product": 25,
33
+ "City": 10,
34
+ "Skill": 20,
35
+ }
36
+
37
+ edge_types = ["WORKS_AT", "LIVES_IN", "KNOWS", "MANAGES", "USES", "SELLS", "HAS_SKILL"]
38
+
39
+ nodes = []
40
+ node_ids = []
41
+ for label, count in node_types.items():
42
+ for i in range(count):
43
+ nid = f"{label.lower()}_{i}"
44
+ node_ids.append(nid)
45
+ nodes.append(
46
+ {
47
+ "id": nid,
48
+ "labels": [label],
49
+ "name": f"{label} {i}",
50
+ }
51
+ )
52
+
53
+ edges = []
54
+ for _ in range(250):
55
+ src = random.choice(node_ids)
56
+ tgt = random.choice(node_ids)
57
+ if src != tgt:
58
+ edges.append(
59
+ {
60
+ "source": src,
61
+ "target": tgt,
62
+ "label": random.choice(edge_types),
63
+ "type": random.choice(edge_types),
64
+ }
65
+ )
66
+
67
+ graph = Graph.from_dict(
68
+ {"nodes": nodes, "edges": edges},
69
+ width=1000,
70
+ height=700,
71
+ )
72
+
73
+ graph # noqa: B018
74
+ return (graph,)
75
+
76
+
77
+ @app.cell
78
+ def _(graph, mo):
79
+ mo.md(f"""
80
+ **Selected node:** {graph.selected_node}
81
+
82
+ Try:
83
+ - Open the Filter panel (sidebar button) to toggle node/edge types
84
+ - Use the search bar to filter (e.g. type "Person" or "Company")
85
+ - Zoom in/out to see label LOD
86
+ """)
87
+ return ()
88
+
89
+
90
+ if __name__ == "__main__":
91
+ app.run()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "anywidget-graph"
3
- version = "0.2.7"
3
+ version = "0.2.9"
4
4
  description = "Interactive graph visualization for Python notebooks using anywidget"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -30,7 +30,7 @@ dev = [
30
30
  "prek>=0.3",
31
31
  "pytest>=9.0.2",
32
32
  "ruff>=0.15",
33
- "ty>=0.0.20",
33
+ "ty>=0.0",
34
34
  "marimo>=0.20",
35
35
  ]
36
36
  networkx = ["networkx>=3.0"]
@@ -3,4 +3,4 @@
3
3
  from anywidget_graph.widget import Graph
4
4
 
5
5
  __all__ = ["Graph"]
6
- __version__ = "0.2.7"
6
+ __version__ = "0.2.8"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING, Any, ClassVar
6
6
 
7
7
  from anywidget_graph.converters import (
8
8
  get_node_id,
@@ -19,6 +19,14 @@ if TYPE_CHECKING:
19
19
  class GrafeoBackend:
20
20
  """Backend for Grafeo database connections."""
21
21
 
22
+ _LANGUAGE_METHODS: ClassVar[dict[str, str]] = {
23
+ "cypher": "execute_cypher",
24
+ "gremlin": "execute_gremlin",
25
+ "graphql": "execute_graphql",
26
+ "sparql": "execute_sparql",
27
+ "sql": "execute_sql",
28
+ }
29
+
22
30
  def __init__(self, db: Any) -> None:
23
31
  """Initialize with a GrafeoDB instance."""
24
32
  self._db = db
@@ -30,7 +38,11 @@ class GrafeoBackend:
30
38
 
31
39
  def execute(self, query: str, *, language: str = "cypher") -> tuple[list[dict], list[dict]]:
32
40
  """Execute a query in the specified language and return (nodes, edges)."""
33
- result = self._db.execute(query)
41
+ method_name = self._LANGUAGE_METHODS.get(language)
42
+ if method_name and hasattr(self._db, method_name):
43
+ result = getattr(self._db, method_name)(query)
44
+ else:
45
+ result = self._db.execute(query)
34
46
  return self._process_result(result)
35
47
 
36
48
  def fetch_schema(self) -> tuple[list[dict], list[dict]]:
@@ -61,7 +73,14 @@ class GrafeoBackend:
61
73
  for record in records:
62
74
  items = self._extract_items(record)
63
75
  for _key, value in items:
64
- if is_node(value):
76
+ if self._is_grafeo_node(value):
77
+ node_dict = self._grafeo_node_to_dict(value)
78
+ node_id = node_dict["id"]
79
+ if node_id not in nodes:
80
+ nodes[node_id] = node_dict
81
+ elif self._is_grafeo_relationship(value):
82
+ edges.append(self._grafeo_relationship_to_dict(value))
83
+ elif is_node(value):
65
84
  node_id = get_node_id(value)
66
85
  if node_id not in nodes:
67
86
  nodes[node_id] = node_to_dict(value)
@@ -70,6 +89,45 @@ class GrafeoBackend:
70
89
 
71
90
  return list(nodes.values()), edges
72
91
 
92
+ @staticmethod
93
+ def _is_grafeo_node(obj: Any) -> bool:
94
+ """Check if a value is a Grafeo node dict (has ``_id`` and ``_labels``)."""
95
+ return isinstance(obj, dict) and "_id" in obj and "_labels" in obj
96
+
97
+ @staticmethod
98
+ def _is_grafeo_relationship(obj: Any) -> bool:
99
+ """Check if a value is a Grafeo relationship dict (has ``_source``, ``_target``, ``_type``)."""
100
+ return isinstance(obj, dict) and "_source" in obj and "_target" in obj and "_type" in obj
101
+
102
+ @staticmethod
103
+ def _grafeo_node_to_dict(node: dict) -> dict:
104
+ """Convert a Grafeo node dict to the widget node format."""
105
+ node_id = str(node["_id"])
106
+ labels = node.get("_labels", [])
107
+ result: dict = {"id": node_id}
108
+ if labels:
109
+ result["label"] = labels[0]
110
+ result["labels"] = list(labels)
111
+ for key, value in node.items():
112
+ if not key.startswith("_"):
113
+ result[key] = value
114
+ if "label" not in result and "name" in result:
115
+ result["label"] = result["name"]
116
+ return result
117
+
118
+ @staticmethod
119
+ def _grafeo_relationship_to_dict(rel: dict) -> dict:
120
+ """Convert a Grafeo relationship dict to the widget edge format."""
121
+ result: dict = {
122
+ "source": str(rel["_source"]),
123
+ "target": str(rel["_target"]),
124
+ "label": str(rel["_type"]),
125
+ }
126
+ for key, value in rel.items():
127
+ if not key.startswith("_"):
128
+ result[key] = value
129
+ return result
130
+
73
131
  def _extract_items(self, record: Any) -> list[tuple[str, Any]]:
74
132
  """Extract key-value items from a record."""
75
133
  if hasattr(record, "items"):
@@ -140,6 +140,7 @@ def get_esm() -> str:
140
140
  schema_js = _read_file(_UI_DIR / "schema.js")
141
141
  settings_js = _read_file(_UI_DIR / "settings.js")
142
142
  properties_js = _read_file(_UI_DIR / "properties.js")
143
+ results_js = _read_file(_UI_DIR / "results.js")
143
144
  toolbar_js = _read_file(_UI_DIR / "toolbar.js")
144
145
  index_js = _read_file(_UI_DIR / "index.js")
145
146
 
@@ -173,6 +174,9 @@ import neo4j from "https://cdn.jsdelivr.net/npm/neo4j-driver@5.28.0/lib/browser/
173
174
  // === Properties Panel ===
174
175
  {_strip_imports_exports(properties_js)}
175
176
 
177
+ // === Results Drawer ===
178
+ {_strip_imports_exports(results_js)}
179
+
176
180
  // === Toolbar ===
177
181
  {_resolve_namespaces(toolbar_js)}
178
182
 
@@ -16,4 +16,6 @@ export const ICONS = {
16
16
  zoomOut: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>`,
17
17
  zoomFit: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>`,
18
18
  search: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
19
+ chevronUp: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 15l-6-6-6 6"/></svg>`,
20
+ table: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18"/></svg>`,
19
21
  };