88
99import pydot
1010
11+ from .base import ifnone
12+
1113log = logging .getLogger (__name__ )
1214
1315
@@ -34,15 +36,55 @@ def _report_unmatched_user_props(user_props, kind):
3436
3537
3638def _monkey_patch_for_jupyter (pydot ):
37- # Ensure Dot nstance render in Jupyter
38- # (see pydot/pydot#220)
39- if not hasattr (pydot .Dot , "_repr_svg_" ):
39+ """
40+ Make Dot instance render in Jupyter notebooks.
41+
42+ .. Note::
43+ Had to use ``_repr_html_()`` and not simply ``_repr_svg_()`` because
44+ (due to https://github.com/jupyterlab/jupyterlab/issues/7497)
45+
46+ Old ``_repr_svg_()`` trick was suggested in https://github.com/pydot/pydot/issues/220.
47+
48+ .. TODO:
49+ Render fully client-side with:
50+ https://visjs.github.io/vis-network/docs/network/#importDot
51+ """
52+ import re
53+ import textwrap
54+
55+ if not hasattr (pydot .Dot , "_repr_html_" ):
4056
4157 def make_svg (self ):
42- return self .create_svg ().decode ()
58+ ## Discard everything above and including `<svg>` element.
59+ svg_txt = self .create_svg ().decode ()
60+ m = re .search ("<svg[^>]+>" , svg_txt )
61+ svg_txt = svg_txt [m .end () :]
62+
63+ html = f"""
64+ <div class="svg_container">
65+ <style>
66+ .svg_container {{
67+ { self ._svg_container_styles }
68+ }}
69+ .svg_container SVG {{
70+ { self ._svg_element_styles }
71+ }}
72+ </style>
73+ <script src="http://ariutta.github.io/svg-pan-zoom/dist/svg-pan-zoom.min.js"></script>
74+ <script type="text/javascript">
75+ var scriptTag = document.scripts[document.scripts.length - 1];
76+ var parentTag = scriptTag.parentNode;
77+ svg_el = parentTag.querySelector(".svg_container svg");
78+ svgPanZoom(svg_el, { self ._svg_pan_zoom_json } );
79+ </script>
80+ <svg>
81+ { svg_txt }
82+ </div>
83+ """
84+ return html
4385
4486 # monkey patch class
45- pydot .Dot ._repr_svg_ = make_svg
87+ pydot .Dot ._repr_html_ = make_svg
4688
4789
4890def build_pydot (
@@ -142,11 +184,16 @@ def get_node_name(a):
142184 if executed and nx_node in executed :
143185 kw ["style" ] = "filled"
144186 kw ["fillcolor" ] = fill_color
187+ try :
188+ kw ["URL" ] = f"file://{ inspect .getfile (nx_node .fn )} "
189+ except Exception as ex :
190+ log .debug (
191+ "Ignoring error while inspecting file of %s: %s" , nx_node .fn , ex
192+ )
145193 node = pydot .Node (
146194 name = quote_dot_kws (nx_node .name ),
147195 shape = shape ,
148196 ## NOTE: Jupyter lab is bocking local-urls (e.g. on SVGs).
149- URL = f"file://{ inspect .getfile (nx_node .fn )} " ,
150197 ** kw ,
151198 )
152199
@@ -211,7 +258,14 @@ def supported_plot_formats():
211258 return [".%s" % f for f in pydot .Dot ().formats ]
212259
213260
214- def render_pydot (dot , filename = None , show = False ):
261+ def render_pydot (
262+ dot ,
263+ filename = None ,
264+ show = False ,
265+ svg_pan_zoom_json : str = None ,
266+ svg_element_styles : str = None ,
267+ svg_container_styles : str = None ,
268+ ):
215269 """
216270 Plot a *Graphviz* dot in a matplotlib, in file or return it for Jupyter.
217271
@@ -224,6 +278,22 @@ def render_pydot(dot, filename=None, show=False):
224278 :param show:
225279 If it evaluates to true, opens the diagram in a matplotlib window.
226280 If it equals `-1`, it returns the image but does not open the Window.
281+ :param svg_pan_zoom_json:
282+ arguments controlling the rendering of a zoomable SVG in
283+ Jupyter notebooks, as defined in https://github.com/ariutta/svg-pan-zoom#how-to-use
284+ if `None`, defaults to string::
285+
286+ {controlIconsEnabled: true, zoomScaleSensitivity: 0.4, fit: true}",
287+
288+ :param svg_element_styles:
289+ mostly for sizing the zoomable SVG in Jupyter notebooks.
290+ Inspect & experiment on the html page of the notebook with browser tools.
291+ if `None`, defaults to string::
292+
293+ width: 100%; height: 300px;
294+
295+ :param svg_container_styles:
296+ like `svg_element_styles`, if `None`, defaults to empty string.
227297
228298 :return:
229299 the matplotlib image if ``show=-1``, or the `dot`.
@@ -259,11 +329,26 @@ def render_pydot(dot, filename=None, show=False):
259329
260330 return img
261331
332+ ## Set properties for rendering in Jupyter as zoomable SVG
333+ #
334+ dot ._svg_pan_zoom_json = ifnone (
335+ svg_pan_zoom_json ,
336+ "{controlIconsEnabled: true, zoomScaleSensitivity: 0.4, fit: true}" ,
337+ )
338+ dot ._svg_element_styles = ifnone (svg_element_styles , "width: 100%; height: 300px;" )
339+ dot ._svg_container_styles = ifnone (svg_container_styles , "" )
340+
262341 return dot
263342
264343
265- def legend (filename = None , show = None ):
266- """Generate a legend for all plots (see Plotter.plot() for args)"""
344+ def legend (
345+ filename = None ,
346+ show = None ,
347+ svg_pan_zoom_json : str = None ,
348+ svg_element_styles : str = None ,
349+ svg_container_styles : str = None ,
350+ ):
351+ """Generate a legend for all plots (see :meth:`.Plotter.plot()` for args)"""
267352 import pydot
268353
269354 _monkey_patch_for_jupyter (pydot )
0 commit comments