Skip to content

Commit c485335

Browse files
committed
FEAT(plot,.TC): +_no_plot node/edge attribute
1 parent 13fde53 commit c485335

File tree

3 files changed

+67
-28
lines changed

3 files changed

+67
-28
lines changed

graphtik/base.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ class PlotArgs(NamedTuple):
362362

363363
#: who is the caller
364364
plottable: "Plottable" = None
365-
#: what to plot
365+
#: what to plot (or the "overlay" when calling :meth:`Plottable.plot()`)
366366
graph: "nx.Graph" = None
367367
#: The name of the graph in the dot-file (important for cmaps).
368368
name: str = None
@@ -390,26 +390,14 @@ def clone_or_merge_graph(self, base_graph) -> "PlotArgs":
390390
:return:
391391
the updated plot_args
392392
"""
393-
import networkx as nx
394393

395394
if self.graph:
395+
import networkx as nx
396+
396397
graph = nx.compose(base_graph, self.graph)
397398
else:
398399
graph = base_graph.copy() # cloned, to freely annotate downstream
399400

400-
## Drop any nodes & edges with "_no_plot" attribute
401-
#
402-
graph.remove_nodes_from(
403-
[n for n, no_plot in graph.nodes.data("_no_plot") if no_plot]
404-
)
405-
graph.remove_edges_from(
406-
[
407-
(src, dst)
408-
for src, dst, no_plot in graph.edges.data("_no_plot")
409-
if no_plot
410-
]
411-
)
412-
413401
return self._replace(graph=graph)
414402

415403
def with_defaults(self, *args, **kw) -> "PlotArgs":
@@ -501,6 +489,7 @@ def plot(
501489
override those derrived from :meth:`_make_op_tooltip()` &
502490
:meth:`_make_op_tooltip()`.
503491
- ``_no_plot``: nodes and/or edges skipped from plotting
492+
(see *"Examples:"* section, below)
504493
505494
- "public" attributes: reaching `Graphviz`_ as-is.
506495
@@ -593,7 +582,7 @@ def plot(
593582
594583
To generate the **legend**, see :func:`.legend()`.
595584
596-
**Sample code:**
585+
**Examples:**
597586
598587
>>> from graphtik import compose, operation
599588
>>> from graphtik.modifiers import optional
@@ -619,6 +608,19 @@ def plot(
619608
<a> [fillcolor=wheat, shape=invhouse, style=filled, tooltip="(int) 1"];
620609
...
621610
611+
You may use the :attr:`PlotArgs.graph` overlay to skip certain nodes (or edges)
612+
from the plots:
613+
614+
>>> import networkx as nx
615+
616+
>>> g = nx.DiGraph() # the overlay
617+
>>> to_hide = netop.net.find_op_by_name("sub")
618+
>>> g.add_node(to_hide, _no_plot=True)
619+
>>> dot = netop.plot(graph=g)
620+
>>> assert "<sub>" not in str(dot), str(dot)
621+
622+
.. graphtik::
623+
622624
"""
623625
kw = locals().copy()
624626

graphtik/plot.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,17 @@
1818
from contextlib import contextmanager
1919
from contextvars import ContextVar
2020
from functools import partial
21-
from typing import Any, Callable, List, Mapping, NamedTuple, Optional, Tuple, Union
21+
from typing import (
22+
Any,
23+
Callable,
24+
Collection,
25+
List,
26+
Mapping,
27+
NamedTuple,
28+
Optional,
29+
Tuple,
30+
Union,
31+
)
2232

2333
import jinja2
2434
import networkx as nx
@@ -574,6 +584,8 @@ def build_pydot(self, plot_args: PlotArgs) -> pydot.Dot:
574584
if graph is None:
575585
raise ValueError("At least `graph` to plot must be given!")
576586

587+
graph, steps = self._skip_nodes(graph, steps)
588+
577589
kw = style.kw_graph.copy()
578590
kw.update(
579591
style.kw_pottable_type.get(
@@ -883,6 +895,25 @@ def _add_legend_icon(self, plot_args: PlotArgs, node_args: NodeArgs):
883895
if kw_legend and self.style.kw_legend.get("URL"):
884896
node_args.dot.add_node(pydot.Node(**kw_legend))
885897

898+
def _skip_nodes(
899+
self, graph: nx.Graph, steps: Collection
900+
) -> Tuple[nx.Graph, Collection]:
901+
## Drop any nodes, steps & edges with "_no_plot" attribute.
902+
#
903+
nodes_to_del = {n for n, no_plot in graph.nodes.data("_no_plot") if no_plot}
904+
graph.remove_nodes_from(nodes_to_del)
905+
if steps:
906+
steps = [s for s in steps if s not in nodes_to_del]
907+
graph.remove_edges_from(
908+
[
909+
(src, dst)
910+
for src, dst, no_plot in graph.edges.data("_no_plot")
911+
if no_plot
912+
]
913+
)
914+
915+
return graph, steps
916+
886917
def render_pydot(self, dot: pydot.Dot, filename=None, jupyter_render: str = None):
887918
"""
888919
Render a |pydot.Dot|_ instance with `Graphviz`_ in a file and/or in a matplotlib window.

test/test_plot.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from graphtik import base, compose, network, operation, plot
1515
from graphtik.modifiers import optional
1616
from graphtik.netop import NetworkOperation
17-
from graphtik.network import yield_ops
1817
from graphtik.plot import (
1918
Plotter,
2019
Style,
@@ -407,7 +406,7 @@ def test_plotter_dill():
407406
dill.dumps(plotter)
408407

409408

410-
def func():
409+
def func(a, b):
411410
pass
412411

413412

@@ -452,7 +451,7 @@ def test_node_dot_str0(dot_str_pipeline):
452451
<TD BORDER="1" SIDES="b" ALIGN="left" TOOLTIP="FunctionalOperation(name=&#x27;cu:sto:m&#x27;, needs=[&#x27;edge&#x27;, &#x27;digraph: strict&#x27;], provides=[&#x27;&lt;graph&gt;&#x27;], fn=&#x27;func&#x27;)" TARGET=""
453452
><B>OP:</B> <I>cu:sto:m</I></TD>
454453
</TR><TR>
455-
<TD ALIGN="left" TOOLTIP="def func():&#10; pass" TARGET=""
454+
<TD ALIGN="left" TOOLTIP="def func(a, b):&#10; pass" TARGET=""
456455
><B>FN:</B> test.test_plot.func</TD>
457456
</TR>
458457
</TABLE>>, shape=plain, tooltip=<cu:sto:m>];
@@ -483,29 +482,36 @@ def test_node_dot_str1(dot_str_pipeline, monkeypatch):
483482
overlay = nx.DiGraph()
484483
hidden_op = dot_str_pipeline.net.find_op_by_name("node")
485484
overlay.add_node(hidden_op, _no_plot=True)
486-
dot_str = str(dot_str_pipeline.plot(graph=overlay))
485+
486+
sol = dot_str_pipeline.compute({"edge": 1, "digraph: strict": 2})
487+
dot_str = str(sol.plot(graph=overlay))
488+
487489
print(dot_str)
488490
exp = """
489-
digraph graph_ {
491+
digraph solution_x5_nodes {
490492
fontname=italic;
491-
label=<graph>;
492493
splines=ortho;
493-
<edge> [shape=invhouse];
494-
<digraph&#58; strict> [shape=invhouse];
495-
<&lt;graph&gt;> [shape=house];
496-
<cu&#58;sto&#58;m> [label=<<TABLE CELLBORDER="0" CELLSPACING="0" STYLE="rounded">
494+
subgraph "cluster_after pruning" {
495+
label="after pruning";
496+
<edge> [fillcolor=wheat, shape=invhouse, style=filled, tooltip="(int) 1"];
497+
<digraph&#58; strict> [fillcolor=wheat, shape=invhouse, style=filled, tooltip="(int) 2"];
498+
<&lt;graph&gt;> [fillcolor=SkyBlue, shape=house, style=filled, tooltip=None];
499+
<cu&#58;sto&#58;m> [label=<<TABLE CELLBORDER="0" CELLSPACING="0" STYLE="rounded" BGCOLOR="wheat">
497500
<TR>
498501
<TD BORDER="1" SIDES="b" ALIGN="left" TOOLTIP="FunctionalOperation(name=&#x27;cu:sto:m&#x27;, needs=[&#x27;edge&#x27;, &#x27;digraph: strict&#x27;], provides=[&#x27;&lt;graph&gt;&#x27;], fn=&#x27;func&#x27;)" HREF="abc#{&#x27;dot_path&#x27;: &#x27;test.test_plot.func&#x27;, &#x27;posix_path&#x27;: &#x27;test/test_plot/func&#x27;}" TARGET="bad"
499502
><B>OP:</B> <I>cu:sto:m</I></TD>
500503
</TR><TR>
501-
<TD ALIGN="left" TOOLTIP="def func():&#10; pass" HREF="abc#{&#x27;dot_path&#x27;: &#x27;test.test_plot.func&#x27;, &#x27;posix_path&#x27;: &#x27;test/test_plot/func&#x27;}" TARGET="bad"
504+
<TD ALIGN="left" TOOLTIP="def func(a, b):&#10; pass" HREF="abc#{&#x27;dot_path&#x27;: &#x27;test.test_plot.func&#x27;, &#x27;posix_path&#x27;: &#x27;test/test_plot/func&#x27;}" TARGET="bad"
502505
><B>FN:</B> test.test_plot.func</TD>
503506
</TR>
504507
</TABLE>>, shape=plain, tooltip=<cu:sto:m>];
508+
}
509+
505510
<edge> -> <cu&#58;sto&#58;m>;
506511
<digraph&#58; strict> -> <cu&#58;sto&#58;m>;
507512
<cu&#58;sto&#58;m> -> <&lt;graph&gt;>;
508513
legend [URL="https://graphtik.readthedocs.io/en/latest/_images/GraphtikLegend.svg", fillcolor=yellow, shape=component, style=filled, target=_top];
509514
}
510515
"""
511516
assert _striplines(dot_str) == _striplines(exp)
517+
assert "<node>" not in dot_str

0 commit comments

Comments
 (0)