def run_graph(dot): """ Runs graphviz to see if the syntax is good. """ graph = AGraph() graph = graph.from_string(dot) extension = 'png' graph.draw(path='output.png', prog='dot', format=extension) sys.exit(0)
def __init__(self, filename): self.G = AGraph(filename) xs = [(nodo, nodo.attr.get("initial", None)) for nodo in self.G.iternodes()] xs = [x for x in xs if x[1]] if len(xs) == 0: raise BadInputGraph("Missing 'initial' node") elif len(xs) > 1: raise BadInputGraph("Cannot have two initial nodes") if not any(nodo.attr.get("goal", None) for nodo in self.G.iternodes()): raise BadInputGraph("Missing a goal state '[goal=\"1\"]'") super(DotGraphSearchProblem, self).__init__(xs[0][0]) self.initial_state.attr["shape"] = "doublecircle" for node in self.G.iternodes(): if self.is_goal(node): node.attr["shape"] = "hexagon" node.attr["color"] = "blue" self.seen = set() self.visit(self.initial_state) for edge in self.G.iteredges(): edge.attr["style"] = "dotted" x = edge.attr.get("weight", None) if x: x = int(x) else: x = 1 edge.attr["weight"] = x edge.attr["label"] = x
def construct_directed_graph(collections_data: List[CollectionData]) -> AGraph: graph = AGraph(directed=True, fontzise=10, fontname='Verdana') graph.node_attr['fontname'] = 'Verdana' graph.node_attr['shape'] = 'record' collection_direction_order_ids = [ collection.id for collection in collections_data ] collection_names = [collection.name for collection in collections_data] # draw "y axe" with tree ordering graph.add_nodes_from(collection_names, shape='plaintext', fontsize=14) graph.add_edges_from(zip(collection_names, collection_names[1:])) graph.add_subgraph(collection_names) # combine all pages in one graph for collection_data in collections_data: for page in collection_data.pages: node = get_node(page) graph.add_node(node, label=page.title) # don't include reversed relations: edges = get_edges(page, collection_direction_order_ids) graph.add_edges_from(edges) # align all nodes for one level in one line (include element from "y" line for alignment) one_level_nodes = [collection_data.name] + [ get_node(page) for page in collection_data.pages ] graph.add_subgraph(one_level_nodes, None, rank='same') return graph
def __init__(self): self.evolution = [] # List[str] self._evaluating = None # Dict[Candidate, Set[Requirement]] self._dependencies = defaultdict(set) # Dict[Candidate.name, Counter[Requirement]] self._active_requirements = defaultdict(Counter) self._node_names = {} self._counter = count() self.graph = AGraph( directed=True, rankdir="LR", labelloc="top", labeljust="center", nodesep="0", concentrate="true", ) self.graph.add_node("root", label=":root:", shape="Mdiamond") self._node_names[self._key(None)] = "root" del self.graph.node_attr["label"] self.graph.edge_attr.update({ "arrowhead": "empty", "style": "dashed", "color": "#808080" })
def visualize_streams(goals, constants, filename='graph.pdf'): from pygraphviz import AGraph import subprocess graph = AGraph(strict=True, directed=False) graph.node_attr['style'] = 'filled' graph.node_attr['shape'] = 'circle' graph.node_attr['fontcolor'] = 'black' graph.node_attr['colorscheme'] = 'SVG' graph.edge_attr['colorscheme'] = 'SVG' for constant in constants: graph.add_node(str(constant), shape='box', color='LightGreen') for goal in goals: name = '\n'.join( str(arg) for arg in [goal.predicate.name] + list(goal.args)) #name = str(goal) graph.add_node(name, shape='box', color='LightBlue') for arg in goal.args: if isinstance(arg, AbstractConstant): arg_name = str(arg) graph.add_edge(name, arg_name) #import os #import webbrowser graph.draw(filename, prog='dot') subprocess.Popen('open %s' % filename, shell=True) #os.system(filename) #webbrowser.open(filename) raw_input('Display?') #safe_remove(filename) return graph
def graph(self, backend='graphviz', rankdir='TB'): inputs_nodes, default_values, output_nodes, function_nodes, dummy_nodes, edges = self._graph( ) if backend == 'graphviz': from graphviz import Digraph G = Digraph(strict=False) G.attr(rankdir=rankdir) add_node = G.node add_edge = G.edge elif backend == 'pygraphviz': from pygraphviz import AGraph G = AGraph(rankdir=rankdir, directed=True, strict=False) add_node = G.add_node add_edge = G.add_edge def format_node_label(name, value): if value is None: return f"{name}=None" elif isinstance(value, (int, float, bool)): return f"{name}={str(value)}" elif isinstance(value, str) and len(value) < 10: return f"{name}=\"{str(value)}\"" else: return f"{name}=..." for f_name in function_nodes: add_node(f_name, **self.graph_function_style) for var_name in inputs_nodes: if var_name in self.default_values.keys(): add_node( "input__" + var_name, label=format_node_label(var_name, self.default_values[var_name]), **self.graph_optional_input_style, ) else: add_node("input__" + var_name, label=var_name, **self.graph_input_style) for var_name in output_nodes: add_node("output__" + var_name, label=var_name, **self.graph_output_style) for dn in dummy_nodes: add_node(dn, shape='point') for e in edges: if e.start is None and e.label in inputs_nodes: add_edge("input__" + e.label, e.end) elif e.end is None and e.label in output_nodes: add_edge(e.start, "output__" + e.label) elif e.start in dummy_nodes: add_edge(e.start, e.end) elif e.end in dummy_nodes: add_edge(e.start, e.end, label=e.label, arrowhead='none') else: add_edge(e.start, e.end, label=e.label) return G
def draw(self): """Draw graph with graphviz and save it to self.config['filename']""" default_gv_attributes = { 'node': { 'shape': 'rect', 'fontname': 'PT Sans' }, 'edge': { 'arrowhead': 'open', 'arrowtail': 'open', 'fontname': 'PT Sans' }, 'graph': { 'fontname': 'PT Sans' } } directed = self.config.get('directed', False) attrs = self.config.get('gv_attributes', default_gv_attributes) g = AGraph(directed=directed, strict=False) for k, v in attrs.get('graph', {}).items(): g.graph_attr[k] = v for k, v in attrs.get('node', {}).items(): g.node_attr[k] = v for k, v in attrs.get('edge', {}).items(): g.edge_attr[k] = v self._draw_nodes(g) self._draw_edges(g, self.config.get('natural', False)) return g.string()
def visualize_stream_orders(orders, streams=[], filename='stream_orders' + DEFAULT_EXTENSION): from pygraphviz import AGraph graph = AGraph(strict=True, directed=True) graph.node_attr['style'] = 'filled' graph.node_attr['shape'] = 'box' graph.node_attr['color'] = STREAM_COLOR graph.node_attr['fontcolor'] = 'black' #graph.node_attr['fontsize'] = 12 graph.node_attr['width'] = 0 graph.node_attr['height'] = 0.02 # Minimum height is 0.02 graph.node_attr['margin'] = 0 graph.graph_attr['outputMode'] = 'nodesfirst' graph.graph_attr['dpi'] = 300 streams = set(streams) | set(flatten(orders)) for stream in streams: graph.add_node(str(stream)) for stream1, stream2 in orders: graph.add_edge(str(stream1), str(stream2)) # TODO: could also print the raw values (or a lookup table) # https://stackoverflow.com/questions/3499056/making-a-legend-key-in-graphviz graph.draw(filename, prog='dot') print('Saved', filename) #display_image(filename) return graph
def _render_graph(source_to_targets, node_to_label, metric_name, node_to_value): g = AGraph(directed=True) # Gather all nodes in graph all_nodes = set(source_to_targets.keys()) for targets in source_to_targets.values(): all_nodes.update(targets) # Build graphviz graph for node in all_nodes: if node not in node_to_value: continue cmap = mpl.cm.get_cmap('viridis') rgba = cmap(node_to_value[node]) color_value = mpl.colors.rgb2hex(rgba) if node_to_value[node] > 0.5: font_color = 'black' else: font_color = 'white' g.add_node(node_to_label[node], label=node_to_label[node], fontname='arial', style='filled', fontcolor=font_color, fillcolor=color_value) for source, targets in source_to_targets.items(): for target in targets: if source in node_to_value and target in node_to_value: g.add_edge(node_to_label[source], node_to_label[target]) return g
def convert(graph_to_convert, layout_prog="dot"): if isinstance(graph_to_convert, AGraph): graph = graph_to_convert else: try: graph = AGraph(graph_to_convert) except BaseException as e: raise ValueError( "graph_to_convert must be one of a string, file, or AGraph object" ) from e graph_edges = { e[0] + "->" + e[1]: list37(e.attr.iteritems()) for e in list37(graph.edges_iter()) } graph_nodes = { n: list37(n.attr.iteritems()) for n in list37(graph.nodes_iter()) } svg_graph = graph.draw(prog=layout_prog, format="svg") nodes, edges = SvgParser(svg_graph).get_nodes_and_edges() [e.enrich_from_graph(graph_edges[e.gid]) for e in edges] [n.enrich_from_graph(graph_nodes[n.gid]) for n in nodes.values()] mx_graph = MxGraph(nodes, edges) return mx_graph.value()
def main(): """ The entrypoint for the program. """ global _MESHINERY_GRAPH global _MESHINERY_ARGS # Register the keyboard interrupt and SIGTERM handler signal.signal(signal.SIGINT, handle_sigint) signal.signal(signal.SIGTERM, handle_sigint) args = docopt(USAGE) _MESHINERY_ARGS = args if args['--verbose']: logging.basicConfig(level=logging.DEBUG) else: logging.basicConfig(level=logging.INFO) logging.info('Starting') logging.debug('Args:\n{}'.format(pformat(args))) agraph = AGraph(args['DOT_GRAPH_FILE']) graph = nx.Graph(agraph) _MESHINERY_GRAPH = graph # We intentionally don't take a fresh copy if args['--clean']: clean(graph, dry_run=args['--dry-run'], instance_id=args['--id'], strays=args['--strays']) if args['clean']: return # Arrange network namespaces and crate veth bridges prepare_namespaces(graph, dry_run=args['--dry-run'], instance_id=args['--id']) # Run commands associated with each node try: graph = execute(graph, dry_run=args['--dry-run'], instance_id=args['--id']) _MESHINERY_GRAPH = graph except Exception as e: logging.error('Per-node command execution failed, cleaning up...') logging.debug('Caught exception {} in main(): {}'.format(type(e), e)) clean(graph, dry_run=args['--dry-run'], instance_id=args['--id']) sys.exit(1) logging.info("Meshinery is ready. Press any key to clean up and exit.") input() clean(graph, dry_run=args['--dry-run'], instance_id=args['--id'], strays=args['--strays']) logging.info('Bye!')
def networkx(self): d = self.graph() # FIXME deduplicate from pygraphviz import AGraph import networkx as nx dot = AGraph(strict=True, directed=True) def rec(nodes, parent): for d in nodes: if not isinstance(d, dict): if '(dirty)' in d: d = d.replace('(dirty)', '') dot.add_node(d, label=d, color='red') dot.add_edge(d, parent, color='red') else: dot.add_node(d, label=d) dot.add_edge(d, parent) else: for k in d: if '(dirty)' in k: k = k.replace('(dirty)', '') dot.add_node(k, label=k, color='red') rec(d[k], k) dot.add_edge(k, parent, color='red') else: dot.add_node(k, label=k) rec(d[k], k) dot.add_edge(k, parent) for k in d: dot.add_node(k, label=k) rec(d[k], k) return nx.nx_agraph.from_agraph(dot)
def get_graphviz(self, triple_args_function=None, ignore_properties=VIS_IGNORE_PROPERTIES): """ Create a pygraphviz graph from the tree """ def _id(node): return node.uri.split("/")[-1] g = AGraph(directed=True) triples = list(self.get_triples()) nodeset = set( chain.from_iterable((t.subject, t.object) for t in triples)) for n in nodeset: label = "%s: %s" % (n.id, n.word) for k, v in n.__dict__.iteritems(): if k not in ignore_properties: label += "\\n%s: %s" % (k, v) g.add_node(_id(n), label=label) # create edges for triple in triples: kargs = (triple_args_function(triple) if triple_args_function else {}) if 'label' not in kargs: kargs['label'] = triple.predicate g.add_edge(_id(triple.subject), _id(triple.object), **kargs) # some theme options g.graph_attr["rankdir"] = "BT" g.node_attr["shape"] = "rect" g.edge_attr["edgesize"] = 10 g.node_attr["fontsize"] = 10 g.edge_attr["fontsize"] = 10 return g
def gen_graph_bundle_dep(exp_file, idx_map): G = AGraph(directed=True, strict=True) G.graph_attr['splines'] = 'true' G.graph_attr['overlap'] = 'false' #G.graph_attr['ordering'] = 'out' #G.graph_attr['concentrate'] = 'false' keys = [key for key in idx_map.keys()] values = [] for lst in idx_map.values(): for item in lst: if not item in values: values.append(item) kvp_list = [] for key in idx_map.keys(): for val in idx_map[key]: kvp_list.append((key, val)) G.add_nodes_from(keys, color='green', style='filled') for val in values: if val.count('#exist'): G.add_node(val, color='yellow', style='filled') elif val.count('#miss'): G.add_node(val, color='red', style='filled') #G.add_nodes_from(values, color = 'red', style = 'filled') G.add_edges_from(kvp_list, color='blue') G.layout(prog='sfdp') G.draw(exp_file)
def render(self, filename): g = AGraph(strict=False, directed=True) # create nodes for frame_id, node in self.callers.items(): label = "{ %s }" % node g.add_node(frame_id, shape='Mrecord', label=label, fontsize=13, labelfontsize=13) # create edges for frame_id, node in self.callers.items(): child_nodes = [] for child_id in node.child_methods: child_nodes.append(child_id) g.add_edge(frame_id, child_id) # order edges l to r if len(child_nodes) > 1: sg = g.add_subgraph(child_nodes, rank='same') sg.graph_attr['rank'] = 'same' prev_node = None for child_node in child_nodes: if prev_node: sg.add_edge(prev_node, child_node, color="#ffffff") prev_node = child_node g.layout() g.draw(path=filename, prog='dot') print("rendered to %s" % filename) self.clear()
def draw(self, filename='bst.png'): from pygraphviz import AGraph G = AGraph() G.node_attr['shape'] = 'circle' none_count = 0 def __draw_edge(node0: self.Node, node1: self.Node): """ 画一条从node0到node1边 """ nonlocal none_count if node1: G.add_edge(node0.key, node1.key) else: none_node = f'None{none_count}' G.add_node(none_node, shape='point') G.add_edge(node0.key, none_node) none_count += 1 def __draw(root: self.Node): """ 画出以root为根的子树 """ if root is None: return __draw(root.left) __draw_edge(root, root.left) __draw_edge(root, root.right) __draw(root.right) __draw(self.root) G.layout('dot') G.draw(filename)
def test_graph_complete(target, project, invoker): cfg = ymp.get_config() res = invoker.call("make", "-qq", "--dag", target) # Snakemake can't be quietet in version 4.7, and pytest can't be # told to ignore stderr. We work around this by removing the # first line if it is the spurious Snakemake log message if res.output.startswith("Building DAG of jobs..."): _, output = res.output.split("\n", 1) else: output = res.output assert output.startswith("digraph") with open("dat.dot", "w") as out: out.write(output) g = DiGraph(AGraph(output)) n_runs = len(cfg[project].runs) n_start_nodes = len([1 for node, degree in g.in_degree() if degree == 0]) log.info("\nTesting start-nodes ({}) >= runs ({})" "".format(n_start_nodes, n_runs)) # assert n_start_nodes >= n_runs n_symlinks = len([ 1 for node, degree in g.in_degree() if g.node[node]['label'].startswith('symlink_raw_reads') ]) log.info("Testing symlinks ({}) == 2 * runs ({})" "".format(n_symlinks, n_runs)) assert n_symlinks == len(cfg[project].fq_names)
def toSVG(B, name): if B: graph = AGraph(toDot(B)) layout = graph.layout(prog="dot") draw = graph.draw("{}.svg".format(name)) else: print("Give me a better tree")
def plan_library_to_graphviz(planlibrary): """Converts a plan library into a graphviz data structure""" G = AGraph() for a in planlibrary: G.add_edge(a.path[0], a.path[1]) return G
def __init__(self, finite_states): self.states = finite_states nodes = { state : [] for state in finite_states } self.graph = AGraph(nodes, directed=True, strict=False) self.edgesize = len(nodes) / 5.0
def visualize_constraints(constraints, filename='constraint_network.pdf', use_functions=True): from pygraphviz import AGraph graph = AGraph(strict=True, directed=False) graph.node_attr['style'] = 'filled' #graph.node_attr['fontcolor'] = 'black' #graph.node_attr['fontsize'] = 12 graph.node_attr['colorscheme'] = 'SVG' graph.edge_attr['colorscheme'] = 'SVG' #graph.graph_attr['rotate'] = 90 #graph.node_attr['fixedsize'] = True graph.node_attr['width'] = 0 graph.node_attr['height'] = 0.02 # Minimum height is 0.02 graph.node_attr['margin'] = 0 graph.graph_attr['rankdir'] = 'RL' graph.graph_attr['nodesep'] = 0.05 graph.graph_attr['ranksep'] = 0.25 #graph.graph_attr['pad'] = 0 # splines="false"; graph.graph_attr['outputMode'] = 'nodesfirst' graph.graph_attr['dpi'] = 300 functions = set() negated = set() heads = set() for fact in constraints: prefix = get_prefix(fact) if prefix in (EQ, MINIMIZE): functions.add(fact[1]) elif prefix == NOT: negated.add(fact[1]) else: heads.add(fact) heads.update(functions) heads.update(negated) objects = {a for head in heads for a in get_args(head)} optimistic_objects = filter(lambda o: isinstance(o, OptimisticObject), objects) for opt_obj in optimistic_objects: graph.add_node(str(opt_obj), shape='circle', color=PARAMETER_COLOR) for head in heads: if not use_functions and (head in functions): continue # TODO: prune values w/o free parameters? name = str_from_fact(head) if head in functions: color = COST_COLOR elif head in negated: color = NEGATED_COLOR else: color = CONSTRAINT_COLOR graph.add_node(name, shape='box', color=color) for arg in get_args(head): if arg in optimistic_objects: graph.add_edge(name, str(arg)) graph.draw(filename, prog='dot') # neato | dot | twopi | circo | fdp | nop return graph
def plot(table_names=None): """ Plot relationships between columns and tables using Graphviz. Parameters ---------- table_names : iterable of str, optional Names of UrbanSim registered tables to plot. Defaults to all registered tables. Returns ------- graph : pygraphviz.AGraph PyGraphviz graph object. """ if not table_names: # Default to all registered tables. table_names = simulation.list_tables() graph = AGraph(directed=True) graph.graph_attr['fontname'] = 'Sans' graph.graph_attr['fontsize'] = 28 graph.node_attr['shape'] = 'box' graph.node_attr['fontname'] = 'Sans' graph.node_attr['fontcolor'] = 'blue' graph.edge_attr['weight'] = 2 # Add each registered table as a subgraph with columns as nodes. for table_name in table_names: subgraph = graph.add_subgraph(name='cluster_' + table_name, label=table_name, fontcolor='red') table = simulation.get_table(table_name) for column_name in table.columns: full_name = table_name + '.' + column_name subgraph.add_node(full_name, label=column_name) # Iterate over computed columns to build nodes. for key, wrapped_col in simulation._COLUMNS.items(): table_name = key[0] column_name = key[1] # Combine inputs from argument names and argument default values. args = list(wrapped_col._argspec.args) if wrapped_col._argspec.defaults: default_args = list(wrapped_col._argspec.defaults) else: default_args = [] inputs = args[:len(args) - len(default_args)] + default_args # Create edge from each input column to the computed column. for input_name in inputs: full_name = table_name + '.' + column_name graph.add_edge(input_name, full_name) graph.layout(prog='dot') return graph
def draw(diagram: 'Diagram', output_file: 'str') -> None: G = AGraph(**GRAPH_OPTIONS) for node in diagram.components.values(): G.add_node(node.name, label=node.render(), **COMPONENT_OPTIONS) for node in diagram.sections.values(): G.add_node(node.name, label=node.render(), **SECTION_OPTIONS) add_edges(G, diagram) G.write() G.draw(output_file, prog=LAYOUT)
def visualize_stream_plan_bipartite(stream_plan, filename='stream_plan' + DEFAULT_EXTENSION, use_functions=False): from pygraphviz import AGraph graph = AGraph(strict=True, directed=True) graph.node_attr['style'] = 'filled' graph.node_attr['shape'] = 'box' graph.node_attr['fontcolor'] = 'black' #graph.node_attr['fontsize'] = 12 graph.node_attr['width'] = 0 graph.node_attr['height'] = 0.02 # Minimum height is 0.02 graph.node_attr['margin'] = 0 #graph.graph_attr['rankdir'] = 'LR' graph.graph_attr['nodesep'] = 0.1 graph.graph_attr['ranksep'] = 0.25 graph.graph_attr['outputMode'] = 'nodesfirst' graph.graph_attr['dpi'] = 300 # TODO: store these settings as a dictionary def add_fact(fact): head, color = (fact[1], COST_COLOR) if get_prefix(fact) == EQ else ( fact, CONSTRAINT_COLOR) s_fact = str_from_fact(head) graph.add_node(s_fact, color=color) return s_fact def add_stream(stream): color = FUNCTION_COLOR if isinstance(stream, FunctionResult) else STREAM_COLOR s_stream = str(stream.instance) if isinstance( stream, FunctionResult) else str(stream) graph.add_node(s_stream, style='rounded,filled', color=color) # shape: oval, plaintext, polygon, rarrow, cds # style: rounded, filled, bold return s_stream achieved_facts = set() for stream in stream_plan: if not use_functions and isinstance(stream, FunctionResult): continue s_stream = add_stream(stream) for fact in stream.instance.get_domain(): if fact in achieved_facts: s_fact = add_fact(fact) graph.add_edge(s_fact, s_stream) # Add initial facts? #if not isinstance(stream, StreamResult): # continue for fact in stream.get_certified(): if fact not in achieved_facts: # Ensures DAG s_fact = add_fact(fact) graph.add_edge(s_stream, s_fact) achieved_facts.add(fact) graph.draw(filename, prog='dot') print('Saved', filename) return graph
def networkx_visualize(self, filename): dgraph = AGraph(strict=False, directed=True) for n in self.subgraph.get_nodes(): self.subgraph._add_nx_subgraph(dgraph, n) dgraph.add_edges_from(self.subgraph.get_viz_edge_list()) dgraph.layout("dot") dgraph.draw(f"{filename}.pdf", format="pdf")
def dot_to_graph(dot, output_path): """ Render by calling graphviz the figure on the output path. :param dot: str with the :param output_path: :return: """ from pygraphviz import AGraph graph = AGraph().from_string(dot) graph.draw(path=output_path, prog='dot')
def __init__(self): logging.basicConfig() self.graph = AGraph(directed=True, strict=False) self.graph.node_attr['shape'] = 'record' self.graph.graph_attr['fontsize'] = '8' self.graph.graph_attr['fontname'] = "Bitstream Vera Sans" self.graph.graph_attr['label'] = "" self.connected_nodes = set() self.described_nodes = set()
def processLines(self): if len(self.lines) == 0: return self.identifier = self.lines[0][2:] s = '\n'.join(self.lines) A = AGraph() G = A.from_string(s) self.processGraph(G)
def dot_to_plan_library(filename): G = AGraph() G.read(filename) planlib = set([]) for edge in G.edges(): a = Action(list(edge)) planlib.add(a) log.info("Loaded graph: " + str(planlib)) return planlib
def cm_json_to_graph(cm_json): """Return pygraphviz Agraph from Kappy's contact map JSON. Parameters ---------- cm_json : dict A JSON dict which contains a contact map generated by Kappy. Returns ------- graph : pygraphviz.Agraph A graph representing the contact map. """ cmap_data = get_cmap_data_from_json(cm_json) # Initialize the graph graph = AGraph() # In this loop we add sites as nodes and clusters around sites to the # graph. We also collect edges to be added between sites later. edges = [] for node_idx, node in enumerate(cmap_data): sites_in_node = [] for site_idx, site in enumerate(node['node_sites']): # We map the unique ID of the site to its name site_key = (node_idx, site_idx) sites_in_node.append(site_key) graph.add_node(site_key, label=site['site_name'], style='filled', shape='ellipse') # Each port link is an edge from the current site to the # specified site if not site['site_type'] or not site['site_type'][0] == 'port': continue # As of kappy 4.1.2, the format of port links have changed # Old format: [[1, 0]], New format: [[[0, 1], 0]] for port_link in site['site_type'][1]['port_links']: port_link = tuple([ link[1] if isinstance(link, list) else link for link in port_link ]) if isinstance(port_link, list): port_link = port_link[1] edge = (site_key, tuple(port_link)) edges.append(edge) graph.add_subgraph(sites_in_node, name='cluster_%s' % node['node_type'], label=node['node_type']) # Finally we add the edges between the sites for source, target in edges: graph.add_edge(source, target) return graph