55import io
66import logging
77import os
8+ from typing import Any , Callable , Mapping , Optional , Tuple , Union
89
910import pydot
1011
11- from .base import ifnone
12-
1312log = logging .getLogger (__name__ )
1413
14+ #: A nested dictionary controlling the rendering of graph-plots in Jupyter cells,
15+ #:
16+ #: as those returned from :meth:`.Plotter.plot()` (currently as SVGs).
17+ #: Either modify it in place, or pass another one in the respective methods.
18+ #:
19+ #: The following keys are supported.
20+ #:
21+ #: :param svg_pan_zoom_json:
22+ #: arguments controlling the rendering of a zoomable SVG in
23+ #: Jupyter notebooks, as defined in https://github.com/ariutta/svg-pan-zoom#how-to-use
24+ #: if `None`, defaults to string (also maps supported)::
25+ #:
26+ #: "{controlIconsEnabled: true, zoomScaleSensitivity: 0.4, fit: true}"
27+ #:
28+ #: :param svg_element_styles:
29+ #: mostly for sizing the zoomable SVG in Jupyter notebooks.
30+ #: Inspect & experiment on the html page of the notebook with browser tools.
31+ #: if `None`, defaults to string (also maps supported)::
32+ #:
33+ #: "width: 100%; height: 300px;"
34+ #:
35+ #: :param svg_container_styles:
36+ #: like `svg_element_styles`, if `None`, defaults to empty string (also maps supported).
37+ default_jupyter_render = {
38+ "svg_pan_zoom_json" : "{controlIconsEnabled: true, zoomScaleSensitivity: 0.4, fit: true}" ,
39+ "svg_element_styles" : "width: 100%; height: 300px;" ,
40+ "svg_container_styles" : "" ,
41+ }
42+
43+
44+ def _parse_jupyter_render (dot ) -> Tuple [str , str , str ]:
45+ import json
46+
47+ jupy_cfg : Mapping [str , Any ] = getattr (dot , "_jupyter_render" , None )
48+ if jupy_cfg is None :
49+ jupy_cfg = default_jupyter_render
50+
51+ def parse_value (key : str , parser : Callable ) -> str :
52+ val : Union [Mapping , str ] = jupy_cfg .get (key )
53+ if not val :
54+ val = ""
55+ elif not isinstance (val , str ):
56+ val = parser (val )
57+ return val
58+
59+ def styles_parser (d : Mapping ) -> str :
60+ return "" .join (f"{ key } : { val } ;\n " for key , val in d )
61+
62+ svg_container_styles = parse_value ("svg_container_styles" , styles_parser )
63+ svg_element_styles = parse_value ("svg_element_styles" , styles_parser )
64+ svg_pan_zoom_json = parse_value ("svg_pan_zoom_json" , json .dumps )
65+
66+ return svg_pan_zoom_json , svg_element_styles , svg_container_styles
67+
68+
69+ def _dot2svg (dot ):
70+ """
71+ Monkey-patching for ``pydot.Dot._repr_html_()` for rendering in jupyter cells.
72+
73+ Original ``_repr_svg_()`` trick was suggested in https://github.com/pydot/pydot/issues/220.
74+
75+ .. Note::
76+ Had to use ``_repr_html_()`` and not simply ``_repr_svg_()`` because
77+ (due to https://github.com/jupyterlab/jupyterlab/issues/7497)
78+
79+
80+ .. TODO:
81+ Render in jupyter cells fullly on client-side without SVG, using lib:
82+ https://visjs.github.io/vis-network/docs/network/#importDot
83+
84+ """
85+ pan_zoom_json , element_styles , container_styles = _parse_jupyter_render (dot )
86+ svg_txt = dot .create_svg ().decode ()
87+ html = f"""
88+ <div class="svg_container">
89+ <style>
90+ .svg_container {{
91+ { container_styles }
92+ }}
93+ .svg_container SVG {{
94+ { element_styles }
95+ }}
96+ </style>
97+ <script src="http://ariutta.github.io/svg-pan-zoom/dist/svg-pan-zoom.min.js"></script>
98+ <script type="text/javascript">
99+ var scriptTag = document.scripts[document.scripts.length - 1];
100+ var parentTag = scriptTag.parentNode;
101+ svg_el = parentTag.querySelector(".svg_container svg");
102+ svgPanZoom(svg_el, { pan_zoom_json } );
103+ </script>
104+ { svg_txt }
105+ </</>
106+ """
107+ return html
108+
109+
110+ def _monkey_patch_for_jupyter (pydot ):
111+ """Ensure Dot instance render in Jupyter notebooks. """
112+ if not hasattr (pydot .Dot , "_repr_html_" ):
113+ pydot .Dot ._repr_html_ = _dot2svg
114+
15115
16116def _is_class_value_in_list (lst , cls , value ):
17117 return any (isinstance (i , cls ) and i == value for i in lst )
@@ -35,58 +135,6 @@ def _report_unmatched_user_props(user_props, kind):
35135 log .warning ("Unmatched `%s_props`:\n +--%s" , kind , unmatched )
36136
37137
38- def _monkey_patch_for_jupyter (pydot ):
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_" ):
56-
57- def make_svg (self ):
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
85-
86- # monkey patch class
87- pydot .Dot ._repr_html_ = make_svg
88-
89-
90138def build_pydot (
91139 graph ,
92140 steps = None ,
@@ -258,14 +306,7 @@ def supported_plot_formats():
258306 return [".%s" % f for f in pydot .Dot ().formats ]
259307
260308
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- ):
309+ def render_pydot (dot , filename = None , show = False , jupyter_render : str = None ):
269310 """
270311 Plot a *Graphviz* dot in a matplotlib, in file or return it for Jupyter.
271312
@@ -278,22 +319,10 @@ def render_pydot(
278319 :param show:
279320 If it evaluates to true, opens the diagram in a matplotlib window.
280321 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.
322+ :param jupyter_render:
323+ a nested dictionary controlling the rendering of graph-plots in Jupyter cells.
324+ If `None`, defaults to :data:`default_jupyter_render`
325+ (you may modify those in place and they will apply for all future calls).
297326
298327 :return:
299328 the matplotlib image if ``show=-1``, or the `dot`.
@@ -329,25 +358,13 @@ def render_pydot(
329358
330359 return img
331360
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 , "" )
361+ ## Propagate any properties for rendering in Jupyter cells.
362+ dot ._jupyter_render = jupyter_render
340363
341364 return dot
342365
343366
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- ):
367+ def legend (filename = None , show = None , jupyter_render : Optional [Mapping ] = None ):
351368 """Generate a legend for all plots (see :meth:`.Plotter.plot()` for args)"""
352369 import pydot
353370
0 commit comments