From ac7bc92f25b2c298b92a0673aafcd26d22fd2a6c Mon Sep 17 00:00:00 2001 From: Moritz Lampert Date: Tue, 10 Feb 2026 12:02:57 +0000 Subject: [PATCH 1/2] make local HTML self-contained --- src/pathpyG/visualisations/_d3js/backend.py | 80 ++++++++++++------- .../visualisations/_d3js/templates/setup.js | 28 ------- 2 files changed, 50 insertions(+), 58 deletions(-) delete mode 100644 src/pathpyG/visualisations/_d3js/templates/setup.js diff --git a/src/pathpyG/visualisations/_d3js/backend.py b/src/pathpyG/visualisations/_d3js/backend.py index 76c56bc76..cf317d4c5 100644 --- a/src/pathpyG/visualisations/_d3js/backend.py +++ b/src/pathpyG/visualisations/_d3js/backend.py @@ -21,7 +21,6 @@ import uuid import webbrowser from copy import deepcopy -from string import Template from pathpyG.utils.config import config from pathpyG.visualisations.network_plot import NetworkPlot @@ -39,7 +38,7 @@ TemporalNetworkPlot: "temporal", TimeUnfoldedNetworkPlot: "unfolded", } -_CDN_URL = "https://d3js.org/d3.v7.min.js" +_CDN_URL = "https://cdn.jsdelivr.net/npm/d3@7/+esm" class D3jsBackend(PlotBackend): @@ -116,8 +115,8 @@ def save(self, filename: str) -> None: - Embedded in websites or documentation - Shared without additional dependencies """ - # Default to the CDN version of d3js since browsers may block local scripts - self.config["d3js_local"] = self.config.get("d3js_local", False) + # Default to embedded local version to obtain a self-contained file + self.config["d3js_local"] = config.get("d3js_local", True) with open(filename, "w+") as new: new.write(self.to_html()) @@ -133,13 +132,13 @@ def show(self) -> None: and choose appropriate display method automatically. """ # Default to CDN version if reachable - # Check if CDN is reachable try: - urllib.request.urlopen(_CDN_URL, timeout=2) - self.config["d3js_local"] = self.config.get("d3js_local", False) + # Attempt to access the CDN URL to check if it's reachable + urllib.request.urlopen(urllib.request.Request(_CDN_URL, headers={"User-Agent": "Mozilla/5.0"}), timeout=2) + self.config["d3js_local"] = config.get("d3js_local", False) except (urllib.error.URLError, urllib.error.HTTPError): - self.config["d3js_local"] = self.config.get("d3js_local", True) - + self.config["d3js_local"] = config.get("d3js_local", True) + if config["environment"]["interactive"]: from IPython.display import display_html, HTML # noqa I001 @@ -168,15 +167,21 @@ def _prepare_data(self) -> dict: **Edges**: Include uid, source/target references, and styling """ node_data = self.data["nodes"].copy() - node_data["uid"] = self.data["nodes"].index.map(lambda x: f"({x[0]},{x[1]})" if isinstance(x, tuple) else str(x)) + node_data["uid"] = self.data["nodes"].index.map( + lambda x: f"({x[0]},{x[1]})" if isinstance(x, tuple) else str(x) + ) node_data = node_data.rename(columns={"x": "xpos", "y": "ypos"}) if self._kind == "unfolded": node_data["ypos"] = 1 - node_data["ypos"] # Invert y-axis for unfolded layout edge_data = self.data["edges"].copy() edge_data["uid"] = self.data["edges"].index.map(lambda x: f"{x[0]}-{x[1]}") if len(edge_data) > 0: - edge_data["source"] = edge_data.index.to_frame()["source"].map(lambda x: f"({x[0]},{x[1]})" if isinstance(x, tuple) else str(x)) - edge_data["target"] = edge_data.index.to_frame()["target"].map(lambda x: f"({x[0]},{x[1]})" if isinstance(x, tuple) else str(x)) + edge_data["source"] = edge_data.index.to_frame()["source"].map( + lambda x: f"({x[0]},{x[1]})" if isinstance(x, tuple) else str(x) + ) + edge_data["target"] = edge_data.index.to_frame()["target"].map( + lambda x: f"({x[0]},{x[1]})" if isinstance(x, tuple) else str(x) + ) data_dict = { "nodes": node_data.to_dict(orient="records"), "edges": edge_data.to_dict(orient="records"), @@ -253,17 +258,8 @@ def to_html(self) -> str: os.path.normpath("_d3js/templates"), ) - # get d3js library path - if self.config.get("d3js_local", False): - d3js = os.path.join(template_dir, "d3.v7.min.js") - else: - d3js = _CDN_URL - js_template = self.get_template(template_dir) - with open(os.path.join(template_dir, "setup.js")) as template: - setup_template = template.read() - with open(os.path.join(template_dir, "styles.css")) as template: css_template = template.read() @@ -277,17 +273,16 @@ def to_html(self) -> str: # div environment for the plot object html += f'\n
\n' - # add d3js library - html += f'\n' - # start JavaScript html += '" + # add d3js library - either from CDN or as embedded script (local) + if self.config.get("d3js_local", False): + d3js_path = os.path.join(template_dir, "d3.v7.min.js") + with open(d3js_path, "r", encoding="utf-8") as f: + raw_d3_js = f.read() + + # We wrap the local D3 code in an IIFE (Immediately Invoked Function Expression). + # Inside this function, we set 'define' and 'exports' to undefined. + # This forces D3 to ignore VS Code's module system and attach to window.d3. + html += "\n" + html += f"\n" + else: + d3_url = _CDN_URL + html += f""" + + """ + return html def get_template(self, template_dir: str) -> str: diff --git a/src/pathpyG/visualisations/_d3js/templates/setup.js b/src/pathpyG/visualisations/_d3js/templates/setup.js deleted file mode 100644 index 7a146f113..000000000 --- a/src/pathpyG/visualisations/_d3js/templates/setup.js +++ /dev/null @@ -1,28 +0,0 @@ -// Load via requireJS if available (jupyter notebook environment) -try { - // Problem: require.config will raise an exception when called for the second time - require.config({ - paths: { - d3: "$d3js".replace(".js", "") - } - }); - console.log("OKAY: requireJS was detected."); -} -catch(err){ - // a reference error indicates that requireJS does not exist. - // other errors may occur due to multiple calls to config - if (err instanceof ReferenceError){ - console.log("WARNING: NO requireJS was detected!"); - - // Helper function that waits for d3js to be loaded - require = function require(symbols, callback) { - var ms = 10; - window.setTimeout(function(t) { - if (window[symbols[0]]) - callback(window[symbols[0]]); - else - window.setTimeout(arguments.callee, ms); - }, ms); - } - } -}; From 331663687c454f2108c3ee336f961b2aae39128f Mon Sep 17 00:00:00 2001 From: Moritz Lampert Date: Tue, 10 Feb 2026 12:10:31 +0000 Subject: [PATCH 2/2] fix unit test --- tests/visualisations/_d3js/test_backend.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/visualisations/_d3js/test_backend.py b/tests/visualisations/_d3js/test_backend.py index ca6436ef6..61d74af2d 100644 --- a/tests/visualisations/_d3js/test_backend.py +++ b/tests/visualisations/_d3js/test_backend.py @@ -8,6 +8,7 @@ import pytest +from pathpyG import config from pathpyG.core.graph import Graph from pathpyG.core.temporal_graph import TemporalGraph from pathpyG.visualisations._d3js.backend import D3jsBackend @@ -359,11 +360,10 @@ def test_save_creates_html_file(self): assert len(content) > 0 assert "