Skip to content

Commit 5084c6d

Browse files
committed
REFACT(PLOT.jupyter): consolidate 3 plot-args into a dicvt
+ redorder plot module.
1 parent 04ddbb1 commit 5084c6d

File tree

3 files changed

+123
-118
lines changed

3 files changed

+123
-118
lines changed

docs/source/plotting.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ If you want the bare-bone diagram, plot network::
5858

5959
You may increase the height of the SVG cell output with something like this::
6060

61-
graphop.plot(svg_element_styles="height: 600px; width: 100%")
61+
graphop.plot(jupyter_render={"svg_element_styles": "height: 600px; width: 100%"})
6262

63+
Check :data:`.default_jupyter_render` for defaults.
6364

6465

6566
.. _debugging:

graphtik/base.py

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,11 @@
44

55
import abc
66
import logging
7-
7+
from typing import Mapping, Union
88

99
log = logging.getLogger(__name__)
1010

1111

12-
def ifnone(item, default):
13-
return default if item is None else item
14-
15-
1612
def aslist(i, argname, allowed_types=list):
1713
"""Utility to accept singular strings as lists, and None --> []."""
1814
if not i:
@@ -132,6 +128,7 @@ def jetsam(ex, locs, *salvage_vars: str, annotation="jetsam", **salvage_mappings
132128
raise # noqa #re-raise without ex-arg, not to insert my frame
133129

134130

131+
## Defined here, to avoid subclasses importig `plot` module.
135132
class Plotter(abc.ABC):
136133
"""
137134
Classes wishing to plot their graphs should inherit this and ...
@@ -146,9 +143,7 @@ def plot(
146143
self,
147144
filename=None,
148145
show=False,
149-
svg_pan_zoom_json=None,
150-
svg_element_styles=None,
151-
svg_container_styles=None,
146+
jupyter_render: Union[None, Mapping, str] = None,
152147
**kws,
153148
):
154149
"""
@@ -180,16 +175,11 @@ def plot(
180175
an optional nested dict of Grapvhiz attributes for certain edges
181176
:param clusters:
182177
an optional mapping of nodes --> cluster-names, to group them
183-
:param svg_pan_zoom_json:
184-
arguments controlling the rendering of a zoomable SVG in
185-
Jupyter notebooks, as defined in https://github.com/ariutta/svg-pan-zoom#how-to-use
186-
Defaults apply if `None`; read about the defaults in :func:`.render_pydot()`.
187-
:param svg_element_styles:
188-
mostly for sizing the zoomable SVG in Jupyter notebooks.
189-
Defaults apply if `None`; read about the defaults in :func:`.render_pydot()`, and
190-
inspect & experiment on the html page of the notebook with browser tools.
191-
:param svg_container_styles:
192-
like `svg_element_styles`
178+
:param jupyter_render:
179+
a nested dictionary controlling the rendering of graph-plots in Jupyter cells,
180+
if `None`, defaults to :data:`jupyter_render` (you may modify it in place
181+
and apply for all future calls).
182+
193183
:return:
194184
a `pydot.Dot <https://pypi.org/project/pydot/>`_ instance
195185
@@ -202,6 +192,8 @@ def plot(
202192
203193
graphop.plot(svg_element_styles="height: 600px; width: 100%")
204194
195+
Check :data:`.default_jupyter_render` for defaults.
196+
205197
Note that the `graph` argument is absent - Each Plotter provides
206198
its own graph internally; use directly :func:`.render_pydot()` to provide
207199
a different graph.
@@ -279,12 +271,7 @@ def plot(
279271

280272
dot = self._build_pydot(**kws)
281273
return render_pydot(
282-
dot,
283-
filename=filename,
284-
show=show,
285-
svg_pan_zoom_json=svg_pan_zoom_json,
286-
svg_element_styles=svg_element_styles,
287-
svg_container_styles=svg_container_styles,
274+
dot, filename=filename, show=show, jupyter_render=jupyter_render
288275
)
289276

290277
@abc.abstractmethod

graphtik/plot.py

Lines changed: 110 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,113 @@
55
import io
66
import logging
77
import os
8+
from typing import Any, Callable, Mapping, Optional, Tuple, Union
89

910
import pydot
1011

11-
from .base import ifnone
12-
1312
log = 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

16116
def _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-
90138
def 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

Comments
 (0)