Source code for mevis._internal.layouting

import networkx as _nx
from opencog.type_constructors import AtomSpace as _AtomSpace

from .args import check_arg as _check_arg
from .conversion import convert as _convert


GRAPHVIZ_METHODS = ['dot', 'neato', 'twopi', 'circo', 'fdp', 'sfdp']
NETWORKX_METHODS = [
    'bipartite', 'circular', 'kamada_kawai', 'planar', 'random', 'shell', 'spring',
    'spectral', 'spiral']
LAYOUT_METHODS = GRAPHVIZ_METHODS + NETWORKX_METHODS


[docs]def layout(data, method='neato', scale_x=1.0, scale_y=1.0, mirror_x=False, mirror_y=True, center_x=True, center_y=True, **kwargs): """Calculate a layout for a given NetworkX graph with a chosen method. Parameters ---------- data : NetworkX Graph, NetworkX DiGraph, AtomSpace, list of Atoms Input that gets augmented by a layout in form of x and y coordinates for each node. If the data is an AtomSpace or list of Atoms, it is first converted to a NetworkX graph by implicitely calling :func:`convert` with default arguments. Alternatively, it can also be called explicitly by a user on an AtomSpace for finer control. The result can then be fed into this layout function. method : str Layout method for calculating node coordinates, e.g. a Graphviz layout engine. Possible values: - Graphviz layouts - ``"dot"``: hierarchical layout for directed graphs, see `dot <https://graphviz.org/docs/layouts/dot/>`__ - ``"neato"``: spring model layout for up to 100 nodes, minimizes a global energy function, see `neato <https://graphviz.org/docs/layouts/neato/>`__ - ``"twopi"``: radial layout placing nodes on concentric circles depending on their distance from a root node, see `twopi <https://graphviz.org/docs/layouts/twopi/>`__ - ``"circo"``: circular layout, see `circo <https://graphviz.org/docs/layouts/circo/>`__ - ``"fdp"``: spring model layout for larger graphs, reduces forces with the Fruchterman-Reingold heuristic, see `fdp <https://graphviz.org/docs/layouts/fdp/>`__ - ``"sfdp"``: multiscale version of ``fdp`` for large graphs see `sfdp <https://graphviz.org/docs/layouts/sfdp/>`__ - NetworkX layouts - ``bipartite``: nodes in two straight lines, see `bipartite_layout <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.bipartite_layout.html>`__ - ``circular``: nodes on a circle, see `circular_layout <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.circular_layout.html>`__ - ``kamada_kawai``: using Kamada-Kawai path-length cost-function, see `kamada_kawai_layout <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.kamada_kawai_layout.html>`__ - ``planar``: nodes without edge intersections, fails if the graph is not planar, see `planar_layout <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.planar_layout.html>`__ - ``random``: nodes uniformly at random in the unit square, see `random_layout <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.random_layout.html>`__ - ``shell``: nodes in concentric circles, best with ``nlist`` defining list of nodes for each shell, see `shell_layout <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.shell_layout.html>`__ - ``spectral``: using the eigenvectors of the graph Laplacian, see `spectral_layout <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.spectral_layout.html>`__ - ``spiral``: nodes in a spiral, see `spiral_layout <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.spiral_layout.html>`__ - ``spring``: using Fruchterman-Reingold force-directed algorithm, see `spring_layout <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.spring_layout.html>`__ scale_x : int, float A number to contract or stretch the layout along the x coordinate. scale_y : int, float A number to contract or stretch the layout along the y coordinate. mirror_x : bool if ``True``, the x coordinates are inverted. mirror_y : bool if ``True``, the y coordinates are inverted. center_x : bool if ``True``, the x coordinates are shifted so the extreme values have equal distance to 0. center_y : bool if ``True``, the y coordinates are shifted so the extreme values have equal distance to 0. kwargs Other keyword arguments are forwarded to the layout method. Returns ------- graph: NetworkX Graph or NetworkX DiGraph References ---------- - Graphviz documentation: `layouts <https://graphviz.org/docs/layouts/>`__ - NetworkX documentation: `graphviz_layout <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.nx_agraph.graphviz_layout.html>`__ Note ---- Graphviz layouts are influenced by graph properties, e.g. whether a graph is directed or undirected, or whether the shapes of nodes are circle or rectangle. """ # Argument processing _check_arg(data, 'data', (_nx.Graph, _nx.DiGraph, _AtomSpace, list)) _check_arg(method, 'method', str, LAYOUT_METHODS) _check_arg(scale_x, 'scale_x', (int, float)) _check_arg(scale_y, 'scale_y', (int, float)) _check_arg(mirror_x, 'mirror_x', bool) _check_arg(mirror_y, 'mirror_y', bool) _check_arg(center_x, 'center_x', bool) _check_arg(center_y, 'center_y', bool) # Optional conversion if isinstance(data, (_AtomSpace, list)): data = _convert(data) # Calculate layout positions = calc_layout(data, method, kwargs) # Annotate graph data = annotate(data, positions, mirror_x, mirror_y, center_x, center_y, scale_x, scale_y) return data
def calc_layout(graph, method, kwargs): """Calculate x and y coordinates for each node in the given graph with the chosen method.""" if method in GRAPHVIZ_METHODS: # Calculate a Graphviz layout pos = _nx.nx_agraph.graphviz_layout(graph, method, **kwargs) else: # Prepare a NetworkX layout function = getattr(_nx.drawing.layout, method + '_layout') if method == 'bipartite': kwargs = prepare_bipartite(graph, kwargs) elif method == 'shell': kwargs = prepare_shell(graph, kwargs) # Calculate a NetworkX layout pos = function(graph, **kwargs) # Pre-scale it to become roughly compatible with plotting without user-provided scaling pos = {node: (x*300, y*300) for node, (x, y) in pos.items()} return pos def annotate(data, pos, mirror_x, mirror_y, center_x, center_y, scale_x, scale_y): """Annotate graph with coordinates from the layout and optional corrections.""" nodes = data.nodes sign_x, sign_y = calc_mirror(mirror_x, mirror_y) shift_x, shift_y = calc_shift(pos, center_x, center_y) for uid, (x, y) in pos.items(): node = nodes[uid] node['x'] = (x + shift_x) * scale_x * sign_x node['y'] = (y + shift_y) * scale_y * sign_y return data def calc_mirror(mirror_x, mirror_y): sign_x = -1.0 if mirror_x else 1.0 sign_y = -1.0 if mirror_y else 1.0 return sign_x, sign_y def calc_shift(layout, center_x, center_y): """Shift the x and/or y coordinates so they become centered around zero to improve plots.""" if not center_x and not center_y: # Shift nothing shift_x = shift_y = 0.0 # shift nothing else: # Shift x and/or y min_x = min_y = float('inf') max_x = max_y = float('-inf') for x, y in layout.values(): if x < min_x: min_x = x if x > max_x: max_x = x if y < min_y: min_y = y if y > max_y: max_y = y # Shift x or not if center_x and min_x != float('inf') and max_x != float('-inf'): shift_x = -min_x - (max_x - min_x) / 2.0 else: shift_x = 0.0 # Shift y or not if center_y and min_y != float('inf') and max_y != float('-inf'): shift_y = -min_y - (max_y - min_y) / 2.0 else: shift_y = 0.0 return shift_x, shift_y def prepare_bipartite(graph, kwargs): """Add a default nodes argument based on color to get a plot with two lines of nodes.""" try: all_colors = set(vals.get('color', None) for vals in graph.nodes.values()) one_color = list(all_colors)[0] nodes_of_one_type = [key for key, vals in graph.nodes.items() if vals.get('color', None) == one_color] except Exception: nodes_of_one_type = graph.nodes kwargs['nodes'] = kwargs.get('nodes', nodes_of_one_type) return kwargs def prepare_shell(graph, kwargs): """Add a default nlist argument based on color to get a plot with two shells of nodes.""" try: all_colors = set(vals.get('color', None) for vals in graph.nodes.values()) one_color = list(all_colors)[0] nodes_of_one_type = [key for key, vals in graph.nodes.items() if vals.get('color', None) == one_color] nodes_of_other_type = [key for key in graph.nodes if key not in nodes_of_one_type] nlist = [nodes_of_one_type, nodes_of_other_type] except Exception: nlist = [list(graph.nodes)] kwargs['nlist'] = kwargs.get('nlist', nlist) return kwargs