diff --git a/docs/assets/generated_graphs/advanced_showcase_example.html b/docs/assets/generated_graphs/advanced_showcase_example.html index ca65eac..ed8710b 100644 --- a/docs/assets/generated_graphs/advanced_showcase_example.html +++ b/docs/assets/generated_graphs/advanced_showcase_example.html @@ -1,4 +1,3 @@ - @@ -62,7 +61,7 @@ transform: rotate(-90deg); /* Right arrow for collapsed state */ } - #config-container-content-b1f3989b { + #config-container-content-5eb0ba05 { max-height: 40vh; /* Default expanded max height */ overflow-y: auto; padding: 15px; @@ -71,7 +70,7 @@ transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out; } - .collapsed #config-container-content-b1f3989b { + .collapsed #config-container-content-5eb0ba05 { max-height: 0; padding-top: 0; padding-bottom: 0; @@ -79,80 +78,116 @@ border-bottom: none; /* Hide border when collapsed */ } - #mynetwork-b1f3989b { + #mynetwork-5eb0ba05 { width: 100%; flex-grow: 1; /* Graph takes remaining vertical space */ min-height: 0; /* Important for flex children to shrink */ - /* border-top: 1px solid #e0e0e0; */ /* Optional: if config panel is directly above */ background-color: #ffffff; /* Graph background */ } - /* Basic styling for vis.js config elements to blend better */ - div.vis-configuration-wrapper { - padding: 0; /* Remove default padding if vis.js adds it */ - } - div.vis-configuration-wrapper table { - width: 100%; + .filter-panel { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 15px; + background-color: #e9ecef; + border-bottom: 1px solid #dee2e6; + flex-shrink: 0; } - div.vis-configuration-wrapper table tr td:first-child { - width: 30%; /* Adjust label width */ + .filter-panel label { font-size: 13px; + font-weight: 500; + color: #495057; } - div.vis-configuration-wrapper input[type=text], - div.vis-configuration-wrapper select { - width: 95%; - padding: 6px; - margin: 2px 0; - border: 1px solid #ccc; + .filter-panel input[type=text] { + flex-grow: 1; + padding: 6px 8px; + border: 1px solid #ced4da; border-radius: 4px; - box-sizing: border-box; font-size: 13px; } - div.vis-configuration-wrapper input[type=range] { - width: 60%; /* Adjust slider width */ + .filter-panel button { + padding: 6px 12px; + font-size: 13px; + border-radius: 4px; + border: 1px solid #6c757d; + background-color: #6c757d; + color: white; + cursor: pointer; + } + .filter-panel button:hover { + background-color: #5a6268; } - div.vis-configuration-wrapper .vis-label { - font-size: 13px; - color: #333; + + .info-panel { + padding: 8px 15px; + background-color: #f8f9fa; + font-size: 13px; + color: #495057; + text-align: center; + border-bottom: 1px solid #dee2e6; + flex-shrink: 0; } + /* Basic styling for vis.js config elements to blend better */ + div.vis-configuration-wrapper { padding: 0; } + div.vis-configuration-wrapper table { width: 100%; } + div.vis-configuration-wrapper table tr td:first-child { width: 30%; font-size: 13px; } + div.vis-configuration-wrapper input[type=text], + div.vis-configuration-wrapper select { width: 95%; padding: 6px; margin: 2px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 13px; } + div.vis-configuration-wrapper input[type=range] { width: 60%; } + div.vis-configuration-wrapper .vis-label { font-size: 13px; color: #333; } + -
-
+
+

Configuration

- +
-
+
-
+
+ + + +
+ +
+ Tip: Click a node to isolate it and its neighbors. Click the background to reset the view. +
+ +
diff --git a/docs/assets/generated_graphs/course_prerequisites_example.html b/docs/assets/generated_graphs/course_prerequisites_example.html index 43ea32c..fdc2122 100644 --- a/docs/assets/generated_graphs/course_prerequisites_example.html +++ b/docs/assets/generated_graphs/course_prerequisites_example.html @@ -1,4 +1,3 @@ - @@ -62,7 +61,7 @@ transform: rotate(-90deg); /* Right arrow for collapsed state */ } - #config-container-content-69b42451 { + #config-container-content-d21b99d3 { max-height: 40vh; /* Default expanded max height */ overflow-y: auto; padding: 15px; @@ -71,7 +70,7 @@ transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out; } - .collapsed #config-container-content-69b42451 { + .collapsed #config-container-content-d21b99d3 { max-height: 0; padding-top: 0; padding-bottom: 0; @@ -79,80 +78,116 @@ border-bottom: none; /* Hide border when collapsed */ } - #mynetwork-69b42451 { + #mynetwork-d21b99d3 { width: 100%; flex-grow: 1; /* Graph takes remaining vertical space */ min-height: 0; /* Important for flex children to shrink */ - /* border-top: 1px solid #e0e0e0; */ /* Optional: if config panel is directly above */ background-color: #ffffff; /* Graph background */ } - /* Basic styling for vis.js config elements to blend better */ - div.vis-configuration-wrapper { - padding: 0; /* Remove default padding if vis.js adds it */ - } - div.vis-configuration-wrapper table { - width: 100%; + .filter-panel { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 15px; + background-color: #e9ecef; + border-bottom: 1px solid #dee2e6; + flex-shrink: 0; } - div.vis-configuration-wrapper table tr td:first-child { - width: 30%; /* Adjust label width */ + .filter-panel label { font-size: 13px; + font-weight: 500; + color: #495057; } - div.vis-configuration-wrapper input[type=text], - div.vis-configuration-wrapper select { - width: 95%; - padding: 6px; - margin: 2px 0; - border: 1px solid #ccc; + .filter-panel input[type=text] { + flex-grow: 1; + padding: 6px 8px; + border: 1px solid #ced4da; border-radius: 4px; - box-sizing: border-box; font-size: 13px; } - div.vis-configuration-wrapper input[type=range] { - width: 60%; /* Adjust slider width */ + .filter-panel button { + padding: 6px 12px; + font-size: 13px; + border-radius: 4px; + border: 1px solid #6c757d; + background-color: #6c757d; + color: white; + cursor: pointer; + } + .filter-panel button:hover { + background-color: #5a6268; } - div.vis-configuration-wrapper .vis-label { - font-size: 13px; - color: #333; + + .info-panel { + padding: 8px 15px; + background-color: #f8f9fa; + font-size: 13px; + color: #495057; + text-align: center; + border-bottom: 1px solid #dee2e6; + flex-shrink: 0; } + /* Basic styling for vis.js config elements to blend better */ + div.vis-configuration-wrapper { padding: 0; } + div.vis-configuration-wrapper table { width: 100%; } + div.vis-configuration-wrapper table tr td:first-child { width: 30%; font-size: 13px; } + div.vis-configuration-wrapper input[type=text], + div.vis-configuration-wrapper select { width: 95%; padding: 6px; margin: 2px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 13px; } + div.vis-configuration-wrapper input[type=range] { width: 60%; } + div.vis-configuration-wrapper .vis-label { font-size: 13px; color: #333; } + -
-
+
+

Configuration

- +
-
+
-
+
+ + + +
+ +
+ Tip: Click a node to isolate it and its neighbors. Click the background to reset the view. +
+ +
diff --git a/docs/assets/generated_graphs/cycle_graph_example.html b/docs/assets/generated_graphs/cycle_graph_example.html index 90c20f2..3923090 100644 --- a/docs/assets/generated_graphs/cycle_graph_example.html +++ b/docs/assets/generated_graphs/cycle_graph_example.html @@ -1,4 +1,3 @@ - @@ -62,7 +61,7 @@ transform: rotate(-90deg); /* Right arrow for collapsed state */ } - #config-container-content-574513c8 { + #config-container-content-ee568209 { max-height: 40vh; /* Default expanded max height */ overflow-y: auto; padding: 15px; @@ -71,7 +70,7 @@ transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out; } - .collapsed #config-container-content-574513c8 { + .collapsed #config-container-content-ee568209 { max-height: 0; padding-top: 0; padding-bottom: 0; @@ -79,80 +78,116 @@ border-bottom: none; /* Hide border when collapsed */ } - #mynetwork-574513c8 { + #mynetwork-ee568209 { width: 100%; flex-grow: 1; /* Graph takes remaining vertical space */ min-height: 0; /* Important for flex children to shrink */ - /* border-top: 1px solid #e0e0e0; */ /* Optional: if config panel is directly above */ background-color: #ffffff; /* Graph background */ } - /* Basic styling for vis.js config elements to blend better */ - div.vis-configuration-wrapper { - padding: 0; /* Remove default padding if vis.js adds it */ - } - div.vis-configuration-wrapper table { - width: 100%; + .filter-panel { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 15px; + background-color: #e9ecef; + border-bottom: 1px solid #dee2e6; + flex-shrink: 0; } - div.vis-configuration-wrapper table tr td:first-child { - width: 30%; /* Adjust label width */ + .filter-panel label { font-size: 13px; + font-weight: 500; + color: #495057; } - div.vis-configuration-wrapper input[type=text], - div.vis-configuration-wrapper select { - width: 95%; - padding: 6px; - margin: 2px 0; - border: 1px solid #ccc; + .filter-panel input[type=text] { + flex-grow: 1; + padding: 6px 8px; + border: 1px solid #ced4da; border-radius: 4px; - box-sizing: border-box; font-size: 13px; } - div.vis-configuration-wrapper input[type=range] { - width: 60%; /* Adjust slider width */ + .filter-panel button { + padding: 6px 12px; + font-size: 13px; + border-radius: 4px; + border: 1px solid #6c757d; + background-color: #6c757d; + color: white; + cursor: pointer; + } + .filter-panel button:hover { + background-color: #5a6268; } - div.vis-configuration-wrapper .vis-label { - font-size: 13px; - color: #333; + + .info-panel { + padding: 8px 15px; + background-color: #f8f9fa; + font-size: 13px; + color: #495057; + text-align: center; + border-bottom: 1px solid #dee2e6; + flex-shrink: 0; } + /* Basic styling for vis.js config elements to blend better */ + div.vis-configuration-wrapper { padding: 0; } + div.vis-configuration-wrapper table { width: 100%; } + div.vis-configuration-wrapper table tr td:first-child { width: 30%; font-size: 13px; } + div.vis-configuration-wrapper input[type=text], + div.vis-configuration-wrapper select { width: 95%; padding: 6px; margin: 2px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 13px; } + div.vis-configuration-wrapper input[type=range] { width: 60%; } + div.vis-configuration-wrapper .vis-label { font-size: 13px; color: #333; } + -
-
+
+

Configuration

- +
-
+
-
+
+ + + +
+ +
+ Tip: Click a node to isolate it and its neighbors. Click the background to reset the view. +
+ +
diff --git a/docs/assets/generated_graphs/karate_club_example.html b/docs/assets/generated_graphs/karate_club_example.html index e686672..f9075b2 100644 --- a/docs/assets/generated_graphs/karate_club_example.html +++ b/docs/assets/generated_graphs/karate_club_example.html @@ -1,4 +1,3 @@ - @@ -62,7 +61,7 @@ transform: rotate(-90deg); /* Right arrow for collapsed state */ } - #config-container-content-fdedba83 { + #config-container-content-8279a685 { max-height: 40vh; /* Default expanded max height */ overflow-y: auto; padding: 15px; @@ -71,7 +70,7 @@ transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out; } - .collapsed #config-container-content-fdedba83 { + .collapsed #config-container-content-8279a685 { max-height: 0; padding-top: 0; padding-bottom: 0; @@ -79,80 +78,116 @@ border-bottom: none; /* Hide border when collapsed */ } - #mynetwork-fdedba83 { + #mynetwork-8279a685 { width: 100%; flex-grow: 1; /* Graph takes remaining vertical space */ min-height: 0; /* Important for flex children to shrink */ - /* border-top: 1px solid #e0e0e0; */ /* Optional: if config panel is directly above */ background-color: #ffffff; /* Graph background */ } - /* Basic styling for vis.js config elements to blend better */ - div.vis-configuration-wrapper { - padding: 0; /* Remove default padding if vis.js adds it */ - } - div.vis-configuration-wrapper table { - width: 100%; + .filter-panel { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 15px; + background-color: #e9ecef; + border-bottom: 1px solid #dee2e6; + flex-shrink: 0; } - div.vis-configuration-wrapper table tr td:first-child { - width: 30%; /* Adjust label width */ + .filter-panel label { font-size: 13px; + font-weight: 500; + color: #495057; } - div.vis-configuration-wrapper input[type=text], - div.vis-configuration-wrapper select { - width: 95%; - padding: 6px; - margin: 2px 0; - border: 1px solid #ccc; + .filter-panel input[type=text] { + flex-grow: 1; + padding: 6px 8px; + border: 1px solid #ced4da; border-radius: 4px; - box-sizing: border-box; font-size: 13px; } - div.vis-configuration-wrapper input[type=range] { - width: 60%; /* Adjust slider width */ + .filter-panel button { + padding: 6px 12px; + font-size: 13px; + border-radius: 4px; + border: 1px solid #6c757d; + background-color: #6c757d; + color: white; + cursor: pointer; + } + .filter-panel button:hover { + background-color: #5a6268; } - div.vis-configuration-wrapper .vis-label { - font-size: 13px; - color: #333; + + .info-panel { + padding: 8px 15px; + background-color: #f8f9fa; + font-size: 13px; + color: #495057; + text-align: center; + border-bottom: 1px solid #dee2e6; + flex-shrink: 0; } + /* Basic styling for vis.js config elements to blend better */ + div.vis-configuration-wrapper { padding: 0; } + div.vis-configuration-wrapper table { width: 100%; } + div.vis-configuration-wrapper table tr td:first-child { width: 30%; font-size: 13px; } + div.vis-configuration-wrapper input[type=text], + div.vis-configuration-wrapper select { width: 95%; padding: 6px; margin: 2px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 13px; } + div.vis-configuration-wrapper input[type=range] { width: 60%; } + div.vis-configuration-wrapper .vis-label { font-size: 13px; color: #333; } + -
-
+
+

Configuration

- +
-
+
-
+
+ + + +
+ +
+ Tip: Click a node to isolate it and its neighbors. Click the background to reset the view. +
+ +
diff --git a/docs/assets/generated_graphs/simple_directed_example.html b/docs/assets/generated_graphs/simple_directed_example.html index 9f1ed45..1f260f7 100644 --- a/docs/assets/generated_graphs/simple_directed_example.html +++ b/docs/assets/generated_graphs/simple_directed_example.html @@ -1,4 +1,3 @@ - @@ -62,7 +61,7 @@ transform: rotate(-90deg); /* Right arrow for collapsed state */ } - #config-container-content-249a2ede { + #config-container-content-770005e8 { max-height: 40vh; /* Default expanded max height */ overflow-y: auto; padding: 15px; @@ -71,7 +70,7 @@ transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out; } - .collapsed #config-container-content-249a2ede { + .collapsed #config-container-content-770005e8 { max-height: 0; padding-top: 0; padding-bottom: 0; @@ -79,80 +78,116 @@ border-bottom: none; /* Hide border when collapsed */ } - #mynetwork-249a2ede { + #mynetwork-770005e8 { width: 100%; flex-grow: 1; /* Graph takes remaining vertical space */ min-height: 0; /* Important for flex children to shrink */ - /* border-top: 1px solid #e0e0e0; */ /* Optional: if config panel is directly above */ background-color: #ffffff; /* Graph background */ } - /* Basic styling for vis.js config elements to blend better */ - div.vis-configuration-wrapper { - padding: 0; /* Remove default padding if vis.js adds it */ - } - div.vis-configuration-wrapper table { - width: 100%; + .filter-panel { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 15px; + background-color: #e9ecef; + border-bottom: 1px solid #dee2e6; + flex-shrink: 0; } - div.vis-configuration-wrapper table tr td:first-child { - width: 30%; /* Adjust label width */ + .filter-panel label { font-size: 13px; + font-weight: 500; + color: #495057; } - div.vis-configuration-wrapper input[type=text], - div.vis-configuration-wrapper select { - width: 95%; - padding: 6px; - margin: 2px 0; - border: 1px solid #ccc; + .filter-panel input[type=text] { + flex-grow: 1; + padding: 6px 8px; + border: 1px solid #ced4da; border-radius: 4px; - box-sizing: border-box; font-size: 13px; } - div.vis-configuration-wrapper input[type=range] { - width: 60%; /* Adjust slider width */ + .filter-panel button { + padding: 6px 12px; + font-size: 13px; + border-radius: 4px; + border: 1px solid #6c757d; + background-color: #6c757d; + color: white; + cursor: pointer; + } + .filter-panel button:hover { + background-color: #5a6268; } - div.vis-configuration-wrapper .vis-label { - font-size: 13px; - color: #333; + + .info-panel { + padding: 8px 15px; + background-color: #f8f9fa; + font-size: 13px; + color: #495057; + text-align: center; + border-bottom: 1px solid #dee2e6; + flex-shrink: 0; } + /* Basic styling for vis.js config elements to blend better */ + div.vis-configuration-wrapper { padding: 0; } + div.vis-configuration-wrapper table { width: 100%; } + div.vis-configuration-wrapper table tr td:first-child { width: 30%; font-size: 13px; } + div.vis-configuration-wrapper input[type=text], + div.vis-configuration-wrapper select { width: 95%; padding: 6px; margin: 2px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 13px; } + div.vis-configuration-wrapper input[type=range] { width: 60%; } + div.vis-configuration-wrapper .vis-label { font-size: 13px; color: #333; } + -
-
+
+

Configuration

- +
-
+
-
+
+ + + +
+ +
+ Tip: Click a node to isolate it and its neighbors. Click the background to reset the view. +
+ +
diff --git a/docs/examples/customization.md b/docs/examples/customization.md index 90772dc..259bdfb 100644 --- a/docs/examples/customization.md +++ b/docs/examples/customization.md @@ -83,7 +83,7 @@ The extensive `vis_options` used for this graph (defined as `custom_vis_options` "groups": { /* ... group definitions ... */ }, "interaction": { "navigationButtons": True, - "keyboard": {"enabled": True} + "keyboard": {"enabled": False} }, "physics": { "enabled": True, diff --git a/examples/customization_example.py b/examples/customization_example.py index c5b695e..598871e 100644 --- a/examples/customization_example.py +++ b/examples/customization_example.py @@ -209,7 +209,7 @@ def create_showcase_graph() -> nx.Graph: # type: ignore[type-arg] "navigationButtons": True, "tooltipDelay": 200, "keyboard": { - "enabled": True, + "enabled": False, "speed": {"x": 10, "y": 10, "zoom": 0.05}, "bindToWindow": True, }, diff --git a/pyproject.toml b/pyproject.toml index 1b8cd3a..c885191 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,14 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + [project] name = "nx-vis-visualizer" -version = "0.1.2" +version = "0.2.0" description = "A Python wrapper for rendering NetworkX graphs interactively using vis.js." readme = "README.md" requires-python = ">=3.12" -license = {text = "MIT"} +license = "MIT" keywords = ["networkx", "vis.js", "graph", "visualization", "interactive"] @@ -12,9 +16,10 @@ classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", + # The "License :: ..." classifier is no longer needed with the above key. "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Visualization", "Topic :: Software Development :: Libraries :: Python Modules", ] @@ -43,6 +48,13 @@ notebook = [ "ipython>=9.3.0", ] +[tool.setuptools] +packages = {find = {where = ["src"]}} + +[tool.setuptools.package-data] +"nx_vis_visualizer" = ["*.html", "*.css"] + + [tool.ruff] line-length = 80 lint.select = ["E", "W", "F", "I", "UP", "C4", "B", "A", "RUF"] diff --git a/src/nx_vis_visualizer.egg-info/PKG-INFO b/src/nx_vis_visualizer.egg-info/PKG-INFO index 921ce43..8418c41 100644 --- a/src/nx_vis_visualizer.egg-info/PKG-INFO +++ b/src/nx_vis_visualizer.egg-info/PKG-INFO @@ -1,15 +1,15 @@ Metadata-Version: 2.4 Name: nx-vis-visualizer -Version: 0.1.2 +Version: 0.2.0 Summary: A Python wrapper for rendering NetworkX graphs interactively using vis.js. -License: MIT +License-Expression: MIT Keywords: networkx,vis.js,graph,visualization,interactive Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research -Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Scientific/Engineering :: Visualization Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.12 diff --git a/src/nx_vis_visualizer.egg-info/SOURCES.txt b/src/nx_vis_visualizer.egg-info/SOURCES.txt index 1cda3cd..6a107b0 100644 --- a/src/nx_vis_visualizer.egg-info/SOURCES.txt +++ b/src/nx_vis_visualizer.egg-info/SOURCES.txt @@ -3,6 +3,7 @@ README.md pyproject.toml src/nx_vis_visualizer/__init__.py src/nx_vis_visualizer/core.py +src/nx_vis_visualizer/template.html src/nx_vis_visualizer.egg-info/PKG-INFO src/nx_vis_visualizer.egg-info/SOURCES.txt src/nx_vis_visualizer.egg-info/dependency_links.txt diff --git a/src/nx_vis_visualizer/core.py b/src/nx_vis_visualizer/core.py index dbf53f1..3e2e595 100644 --- a/src/nx_vis_visualizer/core.py +++ b/src/nx_vis_visualizer/core.py @@ -1,15 +1,15 @@ -# src/nx_vis_visualizer/core.py - import json import logging import os import uuid import webbrowser from html import escape +from pathlib import Path # Import Path from typing import Any, TypeVar, cast import networkx as nx +# Using a hardcoded name for clear identification in logs logger = logging.getLogger("nx_vis_visualizer") # Runtime compatible TypeVar for NetworkX graphs @@ -32,202 +32,26 @@ iPythonHtmlClassGlobal = None -HTML_TEMPLATE = """ - - - - - - {html_page_title} - - - - - -
-
-

Configuration

- -
-
- -
-
- -
- - - - -""" +def _load_template() -> str: + """Loads the HTML template from the adjacent file.""" + try: + # Get the path to the template file relative to this script + template_path = Path(__file__).parent / "template.html" + with open(template_path, encoding="utf-8") as f: + return f.read() + except FileNotFoundError: + logger.error( + "Could not find template.html. It should be in the same directory as core.py." + ) + return "Template file not found." + + +# Load the template once when the module is imported +HTML_TEMPLATE = _load_template() + DEFAULT_VIS_OPTIONS = { "autoResize": True, - # height and width will be set by parameters or default to 100% in template "nodes": { "shape": "dot", "size": 16, @@ -257,15 +81,14 @@ "dragView": True, "zoomView": True, "tooltipDelay": 200, - "navigationButtons": False, # Often better to control via custom UI - "keyboard": True, + "navigationButtons": False, + # CHANGE: Configure keyboard to not bind globally + "keyboard": { + "enabled": True, + "bindToWindow": False, + }, }, "layout": {"randomSeed": None, "improvedLayout": True}, - # Example of how to define groups (can be overridden by node attributes) - # "groups": { - # "myGroup1": {"color": {"background": "red"}, "shape": "star"}, - # "myGroup2": {"color": {"background": "blue"}, "borderWidth": 3} - # } } @@ -310,10 +133,10 @@ def nx_to_vis( override_node_properties: dict[str, Any] | None = None, override_edge_properties: dict[str, Any] | None = None, graph_width: str = "100%", - graph_height: str = "95vh", # Default height for the graph container within the page + graph_height: str = "95vh", cdn_js: str = "https://unpkg.com/vis-network/standalone/umd/vis-network.min.js", cdn_css: str = "https://unpkg.com/vis-network/styles/vis-network.min.css", - verbosity: int = 0, # 0: silent (only errors/warnings), 1: info, 2: debug + verbosity: int = 0, ) -> str | IPythonHTMLInstance | None: """ Converts a NetworkX graph to an interactive HTML file using vis.js. @@ -376,9 +199,6 @@ def nx_to_vis( "from": node_ids_map[u_obj], "to": node_ids_map[v_obj], } - # If it's a MultiGraph or MultiDiGraph, vis.js needs unique edge IDs - # if they are to be manipulated individually by ID later. - # We can generate one if not provided. if "id" not in attrs and nx_graph.is_multigraph(): edge_entry["id"] = str(uuid.uuid4()) @@ -399,13 +219,11 @@ def nx_to_vis( edge_entry.update(override_edge_properties) edges_data.append(edge_entry) - # Start with a deep copy of default options to avoid modifying the global default current_options: dict[str, Any] = json.loads( json.dumps(DEFAULT_VIS_OPTIONS) ) if isinstance(nx_graph, nx.DiGraph): - # Ensure 'to' exists before trying to set 'enabled' current_options.setdefault("edges", {}).setdefault( "arrows", {} ).setdefault("to", {})["enabled"] = True @@ -415,13 +233,10 @@ def nx_to_vis( if vis_options: if verbosity >= 2: logger.debug(f"Merging user vis_options: {vis_options}") - _deep_merge_dicts( - vis_options, current_options - ) # User options override defaults + _deep_merge_dicts(vis_options, current_options) if verbosity >= 2: logger.debug(f"Options after user merge: {current_options}") - # Handle hierarchical layout implications for physics hierarchical_options = current_options.get("layout", {}).get("hierarchical") hierarchical_enabled = False if isinstance(hierarchical_options, dict): @@ -462,8 +277,8 @@ def nx_to_vis( edges_json_str=edges_json_str, options_json_str=options_json_str, div_id_suffix=div_id_suffix, - width=graph_width, # These are for the #mynetwork div style if needed by template - height=graph_height, # (currently template uses flex-grow) + width=graph_width, + height=graph_height, cdn_js_url=cdn_js, cdn_css_url=cdn_css, ) diff --git a/src/nx_vis_visualizer/template.html b/src/nx_vis_visualizer/template.html new file mode 100644 index 0000000..0a43385 --- /dev/null +++ b/src/nx_vis_visualizer/template.html @@ -0,0 +1,310 @@ + + + + + + {html_page_title} + + + + + +
+
+

Configuration

+ +
+
+ +
+
+ +
+ + + +
+ +
+ Tip: Click a node to isolate it and its neighbors. Click the background to reset the view. +
+ +
+ + + + diff --git a/tests/test_core.py b/tests/test_core.py index 95696ec..ebb1516 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -60,9 +60,10 @@ def test_nx_to_vis_html_content( assert "" in content assert "vis-network.min.js" in content # Check for vis.js CDN - assert ( - '"id": "1"' in content - ) # Check for node data (assuming node 1 exists) + # Check for new UI element to confirm new template is used + assert "Filter nodes (regex):" in content + # Check for data injection + assert '"id": "1"' in content assert '"label": "Node A"' in content assert '"from": "1", "to": "2"' in content # Check for edge data @@ -96,48 +97,25 @@ def test_digraph_enables_arrows_by_default( simple_digraph: nx.DiGraph, # type: ignore[type-arg] tmp_path: Path, ) -> None: + """Test that DiGraphs get arrows enabled by default.""" output_file = tmp_path / "test_digraph.html" nx_to_vis( simple_digraph, output_filename=str(output_file), show_browser=False ) with open(output_file, encoding="utf-8") as f: content = f.read() - # options_json_str = None - for line in content.splitlines(): - if line.strip().startswith("var optionsObject ="): - # Extract the JSON part: from the first '{' to the last '}' on that line, before the ';' - start_index = line.find("{") - end_index = line.rfind("}") - if ( - start_index != -1 - and end_index != -1 - and end_index > start_index - ): - options_json_str = line[start_index : end_index + 1] - break - - assert options_json_str is not None, ( - "Could not find optionsObject JSON in HTML" - ) - try: - parsed_options = json.loads(options_json_str) - except json.JSONDecodeError as e: - pytest.fail( - f"Failed to parse optionsObject JSON: {e}\nJSON string was: {options_json_str}" - ) - - assert isinstance(parsed_options, dict), ( - "Parsed optionsObject is not a dictionary" - ) + # Use the robust helper function + options_object = _extract_json_object(content, "optionsObject") + assert options_object is not None and isinstance(options_object, dict) # Now check the specific option - edges_options = parsed_options.get("edges", {}) + edges_options = options_object.get("edges", {}) arrows_options = edges_options.get("arrows", {}) to_options = arrows_options.get("to", {}) assert to_options.get("enabled") is True, ( - f"arrows.to.enabled is not True. Options found: {json.dumps(parsed_options, indent=2)}" + f"arrows.to.enabled is not True. Options found: {json.dumps(options_object, indent=2)}" ) @@ -165,6 +143,60 @@ def test_custom_options_are_applied( assert options_object.get("interaction", {}).get("dragNodes") is False +def test_keyboard_options_are_correct_by_default( + simple_graph: nx.Graph, # type: ignore[type-arg] + tmp_path: Path, +) -> None: + """Test that the default keyboard options are correctly set to not bind to window.""" + output_file = tmp_path / "test_keyboard_default.html" + nx_to_vis( + simple_graph, output_filename=str(output_file), show_browser=False + ) + + with open(output_file, encoding="utf-8") as f: + content = f.read() + + options_object = _extract_json_object(content, "optionsObject") + assert options_object is not None and isinstance(options_object, dict) + + interaction_opts = options_object.get("interaction", {}) + keyboard_opts = interaction_opts.get("keyboard", {}) + + expected_keyboard_opts = {"enabled": True, "bindToWindow": False} + assert keyboard_opts == expected_keyboard_opts, ( + f"Default keyboard options are incorrect. Expected {expected_keyboard_opts}, got {keyboard_opts}" + ) + + +def test_keyboard_options_can_be_overridden( + simple_graph: nx.Graph, # type: ignore[type-arg] + tmp_path: Path, +) -> None: + """Test that keyboard options can be fully overridden.""" + output_file = tmp_path / "test_keyboard_override.html" + # Test overriding with a simple boolean + custom_opts = {"interaction": {"keyboard": False}} + nx_to_vis( + simple_graph, + output_filename=str(output_file), + vis_options=custom_opts, + show_browser=False, + ) + + with open(output_file, encoding="utf-8") as f: + content = f.read() + + options_object = _extract_json_object(content, "optionsObject") + assert options_object is not None and isinstance(options_object, dict) + + interaction_opts = options_object.get("interaction", {}) + keyboard_opts = interaction_opts.get("keyboard") + + assert keyboard_opts is False, ( + f"Keyboard options not overridden correctly. Expected False, got {keyboard_opts}" + ) + + @pytest.fixture # type: ignore[misc] def complex_graph_data() -> tuple[nx.Graph, dict[str, Any]]: # type: ignore[type-arg] """ @@ -286,29 +318,28 @@ def test_complex_graph_with_options( assert "Complex Graph Test Page" in content - nodes_array_raw = _extract_json_object(content, "nodesArray") + nodes_array_raw = _extract_json_object(content, "allNodesArray") assert nodes_array_raw is not None and isinstance(nodes_array_raw, list) nodes_array: list[dict[str, Any]] = cast( list[dict[str, Any]], nodes_array_raw - ) # Keep cast if needed after isinstance + ) - edges_array_raw = _extract_json_object(content, "edgesArray") + edges_array_raw = _extract_json_object(content, "allEdgesArray") assert edges_array_raw is not None and isinstance(edges_array_raw, list) edges_array: list[dict[str, Any]] = cast( list[dict[str, Any]], edges_array_raw - ) # Keep cast + ) + # --- END CHANGE --- options_object_raw = _extract_json_object(content, "optionsObject") assert options_object_raw is not None and isinstance( options_object_raw, dict ) - # If MyPy said previous cast was redundant, direct assignment is fine: options_object: dict[str, Any] = options_object_raw # --- Verify Nodes --- assert len(nodes_array) == nx_graph.number_of_nodes() - # Find specific nodes (IDs are strings in vis.js) node1_data = next((n for n in nodes_array if n["id"] == "1"), None) node_beta_data = next((n for n in nodes_array if n["id"] == "Beta"), None) node3_data = next((n for n in nodes_array if n["id"] == "3"), None) @@ -317,7 +348,7 @@ def test_complex_graph_with_options( assert node1_data["label"] == "Alpha" assert node1_data["title"] == "Node A" assert node1_data["color"] == "red" - assert node1_data["shape"] == "star" # This comes from node attribute + assert node1_data["shape"] == "star" assert node1_data["group"] == 0 assert node1_data["size"] == 25 @@ -325,13 +356,11 @@ def test_complex_graph_with_options( assert node_beta_data["label"] == "Beta Node" assert node_beta_data["color"] == "#00FF00" assert node_beta_data["group"] == 1 - assert node_beta_data["x"] == 10 # Check for passed through x,y + assert node_beta_data["x"] == 10 assert node_beta_data["y"] == 20 assert node3_data is not None - assert ( - node3_data["label"] == "Gamma" - ) # Default label is node ID if not specified + assert node3_data["label"] == "Gamma" assert node3_data["group"] == 0 # --- Verify Edges --- @@ -351,9 +380,6 @@ def test_complex_graph_with_options( assert edge_1_beta["color"] == "blue" assert edge_1_beta["dashes"] is True assert edge_1_beta["width"] == 3 - # 'weight' from networkx is often passed as 'value' to vis.js if not explicitly mapped - # or used directly if vis.js options are configured for it. - # Here, it should just be passed as 'weight'. assert edge_1_beta["weight"] == 5 assert edge_beta_3 is not None @@ -367,15 +393,9 @@ def test_complex_graph_with_options( # --- Verify Merged Options --- nodes_options = options_object.get("nodes") - assert isinstance(nodes_options, dict), ( - "'nodes' key missing or not a dict in options_object" - ) - + assert isinstance(nodes_options, dict) font_options = nodes_options.get("font") - assert isinstance(font_options, dict), ( - "'font' key missing or not a dict in options_object['nodes']" - ) - + assert isinstance(font_options, dict) assert font_options.get("size") == 10 assert font_options.get("color") == "darkblue" @@ -402,56 +422,30 @@ def test_complex_graph_with_options( assert isinstance(layout_options, dict) assert layout_options.get("randomSeed") == 123 - # Check group definitions from custom_vis_options - groups_option = options_object.get("groups") - assert isinstance(groups_option, dict), ( - f"Expected 'groups' to be a dict, got {type(groups_option)}" - ) + assert isinstance(groups_option, dict) - group_0_style = groups_option.get("0") # Key is string "0" - assert isinstance(group_0_style, dict), ( - f"Expected groups['0'] to be a dict, got {type(group_0_style)}" - ) - assert ( - group_0_style.get("shape") == "ellipse" - ) # Use .get() for the final access too - - group_0_color = group_0_style.get( - "color", {} - ) # Default to empty dict if 'color' is missing - assert isinstance(group_0_color, dict), ( - f"Expected groups['0']['color'] to be a dict, got {type(group_0_color)}" - ) + group_0_style = groups_option.get("0") + assert isinstance(group_0_style, dict) + assert group_0_style.get("shape") == "ellipse" + group_0_color = group_0_style.get("color", {}) + assert isinstance(group_0_color, dict) assert group_0_color.get("border") == "black" - group_1_style = groups_option.get("1") # Key is string "1" - assert isinstance(group_1_style, dict), ( - f"Expected groups['1'] to be a dict, got {type(group_1_style)}" - ) + group_1_style = groups_option.get("1") + assert isinstance(group_1_style, dict) assert group_1_style.get("shape") == "box" - - group_1_font = group_1_style.get("font", {}) # Default to empty dict - assert isinstance(group_1_font, dict), ( - f"Expected groups['1']['font'] to be a dict, got {type(group_1_font)}" - ) + group_1_font = group_1_style.get("font", {}) + assert isinstance(group_1_font, dict) assert group_1_font.get("color") == "white" # Check that a default option not overridden is still present nodes_options_for_default_check = options_object.get("nodes", {}) assert isinstance(nodes_options_for_default_check, dict) - - # Safely access values from DEFAULT_VIS_OPTIONS default_nodes_options = DEFAULT_VIS_OPTIONS.get("nodes") - assert isinstance(default_nodes_options, dict), ( - "DEFAULT_VIS_OPTIONS['nodes'] is not a dict" - ) - + assert isinstance(default_nodes_options, dict) default_border_width = default_nodes_options.get("borderWidth") - assert default_border_width is not None, ( - "borderWidth missing in DEFAULT_VIS_OPTIONS['nodes']" - ) - + assert default_border_width is not None assert ( nodes_options_for_default_check.get("borderWidth") == default_border_width diff --git a/uv.lock b/uv.lock index 5e7ee81..3702dad 100644 --- a/uv.lock +++ b/uv.lock @@ -511,8 +511,8 @@ wheels = [ [[package]] name = "nx-vis-visualizer" -version = "0.1.2" -source = { virtual = "." } +version = "0.2.0" +source = { editable = "." } dependencies = [ { name = "networkx" }, ] @@ -541,7 +541,7 @@ requires-dist = [ { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.6.14" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.16.0" }, { name = "networkx", specifier = ">=3.5" }, - { name = "nx-vis-visualizer", marker = "extra == 'dev'", virtual = "." }, + { name = "nx-vis-visualizer", marker = "extra == 'dev'", editable = "." }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.2.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.1.1" },