2121import uuid
2222import webbrowser
2323from copy import deepcopy
24- from string import Template
2524
2625from pathpyG .utils .config import config
2726from pathpyG .visualisations .network_plot import NetworkPlot
3938 TemporalNetworkPlot : "temporal" ,
4039 TimeUnfoldedNetworkPlot : "unfolded" ,
4140}
42- _CDN_URL = "https://d3js.org/d3.v7.min.js "
41+ _CDN_URL = "https://cdn.jsdelivr.net/npm/d3@7/+esm "
4342
4443
4544class D3jsBackend (PlotBackend ):
@@ -116,8 +115,8 @@ def save(self, filename: str) -> None:
116115 - Embedded in websites or documentation
117116 - Shared without additional dependencies
118117 """
119- # Default to the CDN version of d3js since browsers may block local scripts
120- self .config ["d3js_local" ] = self . config .get ("d3js_local" , False )
118+ # Default to embedded local version to obtain a self-contained file
119+ self .config ["d3js_local" ] = config .get ("d3js_local" , True )
121120 with open (filename , "w+" ) as new :
122121 new .write (self .to_html ())
123122
@@ -133,13 +132,13 @@ def show(self) -> None:
133132 and choose appropriate display method automatically.
134133 """
135134 # Default to CDN version if reachable
136- # Check if CDN is reachable
137135 try :
138- urllib .request .urlopen (_CDN_URL , timeout = 2 )
139- self .config ["d3js_local" ] = self .config .get ("d3js_local" , False )
136+ # Attempt to access the CDN URL to check if it's reachable
137+ urllib .request .urlopen (urllib .request .Request (_CDN_URL , headers = {"User-Agent" : "Mozilla/5.0" }), timeout = 2 )
138+ self .config ["d3js_local" ] = config .get ("d3js_local" , False )
140139 except (urllib .error .URLError , urllib .error .HTTPError ):
141- self .config ["d3js_local" ] = self . config .get ("d3js_local" , True )
142-
140+ self .config ["d3js_local" ] = config .get ("d3js_local" , True )
141+
143142 if config ["environment" ]["interactive" ]:
144143 from IPython .display import display_html , HTML # noqa I001
145144
@@ -168,15 +167,21 @@ def _prepare_data(self) -> dict:
168167 **Edges**: Include uid, source/target references, and styling
169168 """
170169 node_data = self .data ["nodes" ].copy ()
171- node_data ["uid" ] = self .data ["nodes" ].index .map (lambda x : f"({ x [0 ]} ,{ x [1 ]} )" if isinstance (x , tuple ) else str (x ))
170+ node_data ["uid" ] = self .data ["nodes" ].index .map (
171+ lambda x : f"({ x [0 ]} ,{ x [1 ]} )" if isinstance (x , tuple ) else str (x )
172+ )
172173 node_data = node_data .rename (columns = {"x" : "xpos" , "y" : "ypos" })
173174 if self ._kind == "unfolded" :
174175 node_data ["ypos" ] = 1 - node_data ["ypos" ] # Invert y-axis for unfolded layout
175176 edge_data = self .data ["edges" ].copy ()
176177 edge_data ["uid" ] = self .data ["edges" ].index .map (lambda x : f"{ x [0 ]} -{ x [1 ]} " )
177178 if len (edge_data ) > 0 :
178- edge_data ["source" ] = edge_data .index .to_frame ()["source" ].map (lambda x : f"({ x [0 ]} ,{ x [1 ]} )" if isinstance (x , tuple ) else str (x ))
179- edge_data ["target" ] = edge_data .index .to_frame ()["target" ].map (lambda x : f"({ x [0 ]} ,{ x [1 ]} )" if isinstance (x , tuple ) else str (x ))
179+ edge_data ["source" ] = edge_data .index .to_frame ()["source" ].map (
180+ lambda x : f"({ x [0 ]} ,{ x [1 ]} )" if isinstance (x , tuple ) else str (x )
181+ )
182+ edge_data ["target" ] = edge_data .index .to_frame ()["target" ].map (
183+ lambda x : f"({ x [0 ]} ,{ x [1 ]} )" if isinstance (x , tuple ) else str (x )
184+ )
180185 data_dict = {
181186 "nodes" : node_data .to_dict (orient = "records" ),
182187 "edges" : edge_data .to_dict (orient = "records" ),
@@ -253,17 +258,8 @@ def to_html(self) -> str:
253258 os .path .normpath ("_d3js/templates" ),
254259 )
255260
256- # get d3js library path
257- if self .config .get ("d3js_local" , False ):
258- d3js = os .path .join (template_dir , "d3.v7.min.js" )
259- else :
260- d3js = _CDN_URL
261-
262261 js_template = self .get_template (template_dir )
263262
264- with open (os .path .join (template_dir , "setup.js" )) as template :
265- setup_template = template .read ()
266-
267263 with open (os .path .join (template_dir , "styles.css" )) as template :
268264 css_template = template .read ()
269265
@@ -277,17 +273,16 @@ def to_html(self) -> str:
277273 # div environment for the plot object
278274 html += f'\n <div id = "{ dom_id [1 :]} "> </div>\n '
279275
280- # add d3js library
281- html += f'<script charset="utf-8" src="{ d3js } "></script>\n '
282-
283276 # start JavaScript
284277 html += '<script charset="utf-8">\n '
285278
286- # add setup code to run d3js in multiple environments
287- html += Template (setup_template ).substitute (d3js = d3js )
279+ # add d3 render function with unique name to avoid conflicts
280+ callback_name = f"render_plot_{ uuid .uuid4 ().hex } "
281+ html += f"function { callback_name } () {{\n "
288282
289- # start d3 environment
290- html += "require(['d3'], function(d3){ //START\n "
283+ # define 'd3' inside the scope (standardizing access)
284+ html += " const d3 = window.d3;\n "
285+ html += " if (!d3) { console.error('D3 not loaded'); return; }\n "
291286
292287 # add data and config
293288 html += f"const data = { data_json } \n "
@@ -299,12 +294,37 @@ def to_html(self) -> str:
299294 # add JavaScript
300295 html += js_template
301296
302- # end d3 environment
303- html += "\n }) ; //END\n "
297+ # Close the render function
298+ html += "\n }; //END of Render Function \n "
304299
305300 # end JavaScript
306301 html += "\n </script>"
307302
303+ # add d3js library - either from CDN or as embedded script (local)
304+ if self .config .get ("d3js_local" , False ):
305+ d3js_path = os .path .join (template_dir , "d3.v7.min.js" )
306+ with open (d3js_path , "r" , encoding = "utf-8" ) as f :
307+ raw_d3_js = f .read ()
308+
309+ # We wrap the local D3 code in an IIFE (Immediately Invoked Function Expression).
310+ # Inside this function, we set 'define' and 'exports' to undefined.
311+ # This forces D3 to ignore VS Code's module system and attach to window.d3.
312+ html += "<script>\n "
313+ html += "(function() { var define = undefined; var exports = undefined; \n "
314+ html += raw_d3_js
315+ html += "\n })();\n "
316+ html += "</script>\n "
317+ html += f"<script>{ callback_name } ();</script>\n "
318+ else :
319+ d3_url = _CDN_URL
320+ html += f"""
321+ <script type="module">
322+ import * as d3 from "{ d3_url } ";
323+ window.d3 = d3;
324+ { callback_name } ();
325+ </script>
326+ """
327+
308328 return html
309329
310330 def get_template (self , template_dir : str ) -> str :
0 commit comments