diff options
author | Dan Schult <dschult@colgate.edu> | 2015-09-06 19:02:09 -0400 |
---|---|---|
committer | Dan Schult <dschult@colgate.edu> | 2015-09-06 19:02:09 -0400 |
commit | 8ccaa9ad25622382e38fe536e18db33edcfbe632 (patch) | |
tree | cbc7dc50312432a08921297d549c859f0f6e68bb | |
parent | c92d78917f45a76b2634284e6e3f0f3da8fd2b56 (diff) | |
parent | 2caaf086efe3fbead76657ae2dd92e298ccaabed (diff) | |
download | networkx-v1.10.1.tar.gz |
Merge pull request #1760 from dschult/fix-layoutv1.10.1
Fix layout.py and revert changes to default scales.
-rw-r--r-- | doc/source/reference/drawing.rst | 1 | ||||
-rw-r--r-- | networkx/drawing/layout.py | 285 | ||||
-rw-r--r-- | networkx/drawing/tests/test_layout.py | 115 |
3 files changed, 237 insertions, 164 deletions
diff --git a/doc/source/reference/drawing.rst b/doc/source/reference/drawing.rst index b5a76b7b..41f00aad 100644 --- a/doc/source/reference/drawing.rst +++ b/doc/source/reference/drawing.rst @@ -78,6 +78,7 @@ Graph Layout :toctree: generated/ circular_layout + fruchterman_reingold_layout random_layout shell_layout spring_layout diff --git a/networkx/drawing/layout.py b/networkx/drawing/layout.py index 5d9cc991..3b93bbde 100644 --- a/networkx/drawing/layout.py +++ b/networkx/drawing/layout.py @@ -4,6 +4,11 @@ Layout ****** Node positioning algorithms for graph drawing. + +The default scales and centering for these layouts are +typically squares with side [0, 1] or [0, scale]. +The two circular layout routines (circular_layout and +shell_layout) have size [-1, 1] or [-scale, scale]. """ # Copyright (C) 2004-2015 by # Aric Hagberg <hagberg@lanl.gov> @@ -13,7 +18,8 @@ Node positioning algorithms for graph drawing. # BSD license. import collections import networkx as nx -__author__ = """Aric Hagberg (hagberg@lanl.gov)\nDan Schult(dschult@colgate.edu)""" +__author__ = """\n""".join(['Aric Hagberg <aric.hagberg@gmail.com>', + 'Dan Schult(dschult@colgate.edu)']) __all__ = ['circular_layout', 'random_layout', 'shell_layout', @@ -21,32 +27,13 @@ __all__ = ['circular_layout', 'spectral_layout', 'fruchterman_reingold_layout'] -def process_params(G, center, dim): - # Some boilerplate code. - import numpy as np - - if not isinstance(G, nx.Graph): - empty_graph = nx.Graph() - empty_graph.add_nodes_from(G) - G = empty_graph - - if center is None: - center = np.zeros(dim) - else: - center = np.asarray(center) - - if len(center) != dim: - msg = "length of center coordinates must match dimension of layout" - raise ValueError(msg) - - return G, center - -def random_layout(G, dim=2, center=None): - """Position nodes uniformly at random in the unit square. +def random_layout(G, dim=2, scale=1., center=None): + """Position nodes uniformly at random. For every node, a position is generated by choosing each of dim - coordinates uniformly at random on the interval [0.0, 1.0). + coordinates uniformly at random on the default interval [0.0, 1.0), + or on an interval of length `scale` centered at `center`. NumPy (http://scipy.org) is required for this function. @@ -58,8 +45,11 @@ def random_layout(G, dim=2, center=None): dim : int Dimension of layout. - center : array-like or None - Coordinate pair around which to center the layout. + scale : float (default 1) + Scale factor for positions + + center : array-like (default scale*0.5 in each dim) + Coordinate around which to center the layout. Returns ------- @@ -70,21 +60,17 @@ def random_layout(G, dim=2, center=None): -------- >>> G = nx.lollipop_graph(4, 3) >>> pos = nx.random_layout(G) - """ import numpy as np - G, center = process_params(G, center, dim) shape = (len(G), dim) - pos = np.random.random(shape) + center - pos = pos.astype(np.float32) - pos = dict(zip(G, pos)) - - return pos + pos = np.random.random(shape) * scale + if center is not None: + pos += np.asarray(center) - 0.5 * scale + return dict(zip(G, pos)) -def circular_layout(G, dim=2, scale=1, center=None): - # dim=2 only +def circular_layout(G, dim=2, scale=1., center=None): """Position nodes on a circle. Parameters @@ -94,11 +80,11 @@ def circular_layout(G, dim=2, scale=1, center=None): dim : int Dimension of layout, currently only dim=2 is supported - scale : float - Scale factor for positions + scale : float (default 1) + Scale factor for positions, i.e. radius of circle. - center : array-like or None - Coordinate pair around which to center the layout. + center : array-like (default origin) + Coordinate around which to center the layout. Returns ------- @@ -118,23 +104,18 @@ def circular_layout(G, dim=2, scale=1, center=None): """ import numpy as np - G, center = process_params(G, center, dim) - if len(G) == 0: - pos = {} - elif len(G) == 1: - pos = {G.nodes()[0]: center} - else: - # Discard the extra angle since it matches 0 radians. - theta = np.linspace(0, 1, len(G) + 1)[:-1] * 2 * np.pi - theta = theta.astype(np.float32) - pos = np.column_stack([np.cos(theta), np.sin(theta)]) - pos = _rescale_layout(pos, scale=scale) + center - pos = dict(zip(G, pos)) + return {} - return pos + twopi = 2.0*np.pi + theta = np.arange(0, twopi, twopi/len(G)) + pos = np.column_stack([np.cos(theta), np.sin(theta)]) * scale + if center is not None: + pos += np.asarray(center) -def shell_layout(G, nlist=None, dim=2, scale=1, center=None): + return dict(zip(G, pos)) + +def shell_layout(G, nlist=None, dim=2, scale=1., center=None): """Position nodes in concentric circles. Parameters @@ -147,11 +128,11 @@ def shell_layout(G, nlist=None, dim=2, scale=1, center=None): dim : int Dimension of layout, currently only dim=2 is supported - scale : float - Scale factor for positions + scale : float (default 1) + Scale factor for positions, i.e.radius of largest shell - center : array-like or None - Coordinate pair around which to center the layout. + center : array-like (default origin) + Coordinate around which to center the layout. Returns ------- @@ -172,39 +153,42 @@ def shell_layout(G, nlist=None, dim=2, scale=1, center=None): """ import numpy as np - G, center = process_params(G, center, dim) - if len(G) == 0: return {} - elif len(G) == 1: - return {G.nodes()[0]: center} - if nlist is None: # draw the whole graph in one shell - nlist = [list(G.nodes())] + nlist = [list(G)] + numb_shells = len(nlist) if len(nlist[0]) == 1: # single node at center radius = 0.0 + numb_shells -= 1 else: # else start at r=1 radius = 1.0 + # distance between shells + gap = (scale / numb_shells) if numb_shells else scale + radius *= gap npos={} + twopi = 2.0*np.pi for nodes in nlist: - # Discard the extra angle since it matches 0 radians. - theta = np.linspace(0, 1, len(nodes) + 1)[:-1] * 2 * np.pi - theta = theta.astype(np.float32) - pos = np.column_stack([np.cos(theta), np.sin(theta)]) - pos = _rescale_layout(pos, scale=scale * radius / len(nlist)) + center + theta = np.arange(0, twopi, twopi/len(nodes)) + pos = np.column_stack([np.cos(theta), np.sin(theta)]) * radius npos.update(zip(nodes, pos)) - radius += 1.0 + radius += gap + + if center is not None: + center = np.asarray(center) + for n,p in npos.items(): + npos[n] = p + center return npos -def fruchterman_reingold_layout(G,dim=2,k=None, +def fruchterman_reingold_layout(G, dim=2, k=None, pos=None, fixed=None, iterations=50, @@ -215,7 +199,7 @@ def fruchterman_reingold_layout(G,dim=2,k=None, Parameters ---------- - G : NetworkX graph or list of nodes + G : NetworkX graph dim : int Dimension of layout @@ -225,7 +209,6 @@ def fruchterman_reingold_layout(G,dim=2,k=None, 1/sqrt(n) where n is the number of nodes. Increase this value to move nodes farther apart. - pos : dict or None optional (default=None) Initial positions for nodes as a dictionary with node as keys and values as a list or tuple. If None, then use random initial @@ -233,21 +216,21 @@ def fruchterman_reingold_layout(G,dim=2,k=None, fixed : list or None optional (default=None) Nodes to keep fixed at initial position. + If any nodes are fixed, the scale and center features are not used. iterations : int optional (default=50) Number of iterations of spring-force relaxation weight : string or None optional (default='weight') The edge attribute that holds the numerical value used for - the edge weight. If None, then all edge weights are 1. + the effective spring constant. If None, edge weights are 1. scale : float (default=1.0) Scale factor for positions. The nodes are positioned - in a box of size [0,scale] x [0,scale]. - - center : array-like or None - Coordinate pair around which to center the layout. + in a box of size `scale` in each dim centered at `center`. + center : array-like (default scale/2 in each dim) + Coordinate around which to center the layout. Returns ------- @@ -259,66 +242,62 @@ def fruchterman_reingold_layout(G,dim=2,k=None, >>> G=nx.path_graph(4) >>> pos=nx.spring_layout(G) - # The same using longer function name + # this function has two names: + # spring_layout and fruchterman_reingold_layout >>> pos=nx.fruchterman_reingold_layout(G) """ import numpy as np - G, center = process_params(G, center, dim) + if len(G) == 0: + return {} if fixed is not None: nfixed = dict(zip(G, range(len(G)))) fixed = np.asarray([nfixed[v] for v in fixed]) + if pos is None: + msg = "Keyword pos must be specified if any nodes are fixed" + raise ValueError(msg) + if pos is not None: # Determine size of existing domain to adjust initial positions - dom_size = max(flatten(pos.values())) + pos_coords = np.array(list(pos.values())) + min_coords = pos_coords.min(0) + domain_size = pos_coords.max(0) - min_coords shape = (len(G), dim) - pos_arr = np.random.random(shape) * dom_size + center + pos_arr = np.random.random(shape) * domain_size + min_coords for i,n in enumerate(G): if n in pos: pos_arr[i] = np.asarray(pos[n]) else: pos_arr=None - if len(G) == 0: - return {} - if len(G) == 1: - return {G.nodes()[0]: center} - + if k is None and fixed is not None: + # Adjust k for domains larger than 1x1 + k=domain_size.max()/np.sqrt(len(G)) try: # Sparse matrix if len(G) < 500: # sparse solver for large graphs raise ValueError - A = nx.to_scipy_sparse_matrix(G,weight=weight,dtype='f') - if k is None and fixed is not None: - # We must adjust k by domain size for layouts that are not near 1x1 - nnodes,_ = A.shape - k = dom_size / np.sqrt(nnodes) - pos = _sparse_fruchterman_reingold(A, dim, k, pos_arr, fixed, iterations) + A = nx.to_scipy_sparse_matrix(G, weight=weight, dtype='f') + pos = _sparse_fruchterman_reingold(A,dim,k,pos_arr,fixed,iterations) except: - A = nx.to_numpy_matrix(G,weight=weight) - if k is None and fixed is not None: - # We must adjust k by domain size for layouts that are not near 1x1 - nnodes,_ = A.shape - k = dom_size / np.sqrt(nnodes) + A = nx.to_numpy_matrix(G, weight=weight) pos = _fruchterman_reingold(A, dim, k, pos_arr, fixed, iterations) + if fixed is None: - pos = _rescale_layout(pos, scale=scale) + center - pos = dict(zip(G,pos)) - return pos + pos = _rescale_layout(pos, scale) + if center is not None: + pos += np.asarray(center) - 0.5 * scale + + return dict(zip(G,pos)) spring_layout=fruchterman_reingold_layout -def _fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None, - iterations=50): +def _fruchterman_reingold(A,dim=2,k=None,pos=None,fixed=None,iterations=50): # Position nodes in adjacency matrix A using Fruchterman-Reingold # Entry point for NetworkX graph is fruchterman_reingold_layout() - try: - import numpy as np - except ImportError: - raise ImportError("_fruchterman_reingold() requires numpy: http://scipy.org/ ") - + import numpy as np try: nnodes,_=A.shape except AttributeError: @@ -327,7 +306,7 @@ def _fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None, A=np.asarray(A) # make sure we have an array instead of a matrix - if pos==None: + if pos is None: # random initial positions pos=np.asarray(np.random.random((nnodes,dim)),dtype=A.dtype) else: @@ -339,8 +318,7 @@ def _fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None, k=np.sqrt(1.0/nnodes) # the initial "temperature" is about .1 of domain area (=1x1) # this is the largest step allowed in the dynamics. - # We need to calculate this in case our fixed positions force our domain - # to be much bigger than 1x1 + # Calculate domain in case our fixed positions are bigger than 1x1 t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1]))*0.1 # simple cooling scheme. # linearly step down by dt on each iteration so last iteration is size dt. @@ -363,7 +341,7 @@ def _fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None, .sum(axis=1) # update positions length=np.sqrt((displacement**2).sum(axis=1)) - length=np.where(length<0.01,0.1,length) + length=np.where(length<0.01,0.01,length) delta_pos=np.transpose(np.transpose(displacement)*t/length) if fixed is not None: # don't change positions of fixed nodes @@ -371,6 +349,8 @@ def _fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None, pos+=delta_pos # cool temperature t-=dt + if fixed is None: + pos = _rescale_layout(pos) return pos @@ -379,10 +359,7 @@ def _sparse_fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None, # Position nodes in adjacency matrix A using Fruchterman-Reingold # Entry point for NetworkX graph is fruchterman_reingold_layout() # Sparse version - try: - import numpy as np - except ImportError: - raise ImportError("_sparse_fruchterman_reingold() requires numpy: http://scipy.org/ ") + import numpy as np try: nnodes,_=A.shape except AttributeError: @@ -398,7 +375,7 @@ def _sparse_fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None, except: A=(coo_matrix(A)).tolil() - if pos==None: + if pos is None: # random initial positions pos=np.asarray(np.random.random((nnodes,dim)),dtype=A.dtype) else: @@ -406,7 +383,7 @@ def _sparse_fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None, pos=pos.astype(A.dtype) # no fixed nodes - if fixed==None: + if fixed is None: fixed=[] # optimal distance between nodes @@ -414,7 +391,8 @@ def _sparse_fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None, k=np.sqrt(1.0/nnodes) # the initial "temperature" is about .1 of domain area (=1x1) # this is the largest step allowed in the dynamics. - t=0.1 + # Calculate domain in case our fixed positions are bigger than 1x1 + t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1]))*0.1 # simple cooling scheme. # linearly step down by dt on each iteration so last iteration is size dt. dt=t/float(iterations+1) @@ -439,14 +417,16 @@ def _sparse_fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None, (delta*(k*k/distance**2-Ai*distance/k)).sum(axis=1) # update positions length=np.sqrt((displacement**2).sum(axis=0)) - length=np.where(length<0.01,0.1,length) + length=np.where(length<0.01,0.01,length) pos+=(displacement*t/length).T # cool temperature t-=dt + if fixed is None: + pos = _rescale_layout(pos) return pos -def spectral_layout(G, dim=2, weight='weight', scale=1, center=None): +def spectral_layout(G, dim=2, weight='weight', scale=1., center=None): """Position nodes using the eigenvectors of the graph Laplacian. Parameters @@ -460,11 +440,12 @@ def spectral_layout(G, dim=2, weight='weight', scale=1, center=None): The edge attribute that holds the numerical value used for the edge weight. If None, then all edge weights are 1. - scale : float - Scale factor for positions + scale : float optional (default 1) + Scale factor for positions, i.e. nodes placed in a box with + side [0, scale] or centered on `center` if provided. - center : array-like or None - Coordinate pair around which to center the layout. + center : array-like (default scale/2 in each dim) + Coordinate around which to center the layout. Returns ------- @@ -487,15 +468,18 @@ def spectral_layout(G, dim=2, weight='weight', scale=1, center=None): # handle some special cases that break the eigensolvers import numpy as np - G, center = process_params(G, center, dim) - if len(G) <= 2: if len(G) == 0: - pos = np.array([]) + return {} elif len(G) == 1: - pos = np.array([center]) - else: - pos = np.array([np.zeros(dim), np.array(center)*2.0]) + if center is not None: + pos = np.asarray(center) + else: + pos = np.ones((1,dim)) * scale * 0.5 + else: #len(G) == 2 + pos = np.array([np.zeros(dim), np.ones(dim) * scale]) + if center is not None: + pos += np.asarray(center) - scale * 0.5 return dict(zip(G,pos)) try: # Sparse matrix @@ -514,9 +498,11 @@ def spectral_layout(G, dim=2, weight='weight', scale=1, center=None): A = A + np.transpose(A) pos = _spectral(A, dim) - pos = _rescale_layout(pos, scale) + center - pos = dict(zip(G,pos)) - return pos + pos = _rescale_layout(pos, scale) + if center is not None: + pos += np.asarray(center) - 0.5 * scale + + return dict(zip(G,pos)) def _spectral(A, dim=2): @@ -578,20 +564,19 @@ def _sparse_spectral(A,dim=2): return np.real(eigenvectors[:,index]) -def _rescale_layout(pos,scale=1): - # rescale to (-scale,scale) in all axes +def _rescale_layout(pos, scale=1.): + # rescale to [0, scale) in each axis - # shift origin to (0,0) - lim=0 # max coordinate for all axes - for i in range(pos.shape[1]): - pos[:,i]-=pos[:,i].mean() - lim=max(pos[:,i].max(),lim) - # rescale to (-scale,scale) in all directions, preserves aspect + # Find max length over all dimensions + maxlim=0 for i in range(pos.shape[1]): - pos[:,i]*=scale/lim + pos[:,i] -= pos[:,i].min() # shift min to zero + maxlim = max(maxlim, pos[:,i].max()) + if maxlim > 0: + for i in range(pos.shape[1]): + pos[:,i] *= scale / maxlim return pos - # fixture for nose tests def setup_module(module): from nose import SkipTest @@ -603,17 +588,3 @@ def setup_module(module): import scipy except: raise SkipTest("SciPy not available") - -def flatten(l): - try: - bs = basestring - except NameError: - # Py3k - bs = str - for el in l: - if isinstance(el, collections.Iterable) and not isinstance(el, bs): - for sub in flatten(el): - yield sub - else: - yield el - diff --git a/networkx/drawing/tests/test_layout.py b/networkx/drawing/tests/test_layout.py index bcd6d236..8366d252 100644 --- a/networkx/drawing/tests/test_layout.py +++ b/networkx/drawing/tests/test_layout.py @@ -32,14 +32,115 @@ class TestLayout(object): vpos=nx.shell_layout(G) def test_smoke_string(self): - G=self.Gs - vpos=nx.random_layout(G) - vpos=nx.circular_layout(G) - vpos=nx.spring_layout(G) - vpos=nx.fruchterman_reingold_layout(G) - vpos=nx.spectral_layout(G) - vpos=nx.shell_layout(G) + G = self.Gs + vpos = nx.random_layout(G) + vpos = nx.circular_layout(G) + vpos = nx.spring_layout(G) + vpos = nx.fruchterman_reingold_layout(G) + vpos = nx.spectral_layout(G) + vpos = nx.shell_layout(G) + + def test_empty_graph(self): + G=nx.Graph() + vpos = nx.random_layout(G) + vpos = nx.circular_layout(G) + vpos = nx.spring_layout(G) + vpos = nx.fruchterman_reingold_layout(G) + vpos = nx.shell_layout(G) + vpos = nx.spectral_layout(G) + # center arg + vpos = nx.random_layout(G, scale=2, center=(4,5)) + vpos = nx.circular_layout(G, scale=2, center=(4,5)) + vpos = nx.spring_layout(G, scale=2, center=(4,5)) + vpos = nx.shell_layout(G, scale=2, center=(4,5)) + vpos = nx.spectral_layout(G, scale=2, center=(4,5)) + + def test_single_node(self): + G = nx.Graph() + G.add_node(0) + vpos = nx.random_layout(G) + vpos = nx.circular_layout(G) + vpos = nx.spring_layout(G) + vpos = nx.fruchterman_reingold_layout(G) + vpos = nx.shell_layout(G) + vpos = nx.spectral_layout(G) + # center arg + vpos = nx.random_layout(G, scale=2, center=(4,5)) + vpos = nx.circular_layout(G, scale=2, center=(4,5)) + vpos = nx.spring_layout(G, scale=2, center=(4,5)) + vpos = nx.shell_layout(G, scale=2, center=(4,5)) + vpos = nx.spectral_layout(G, scale=2, center=(4,5)) + + def check_scale_and_center(self, pos, scale, center): + center = numpy.array(center) + low = center - 0.5 * scale + hi = center + 0.5 * scale + vpos = numpy.array(list(pos.values())) + length = vpos.max(0) - vpos.min(0) + assert (length <= scale).all() + assert (vpos >= low).all() + assert (vpos <= hi).all() + + def test_scale_and_center_arg(self): + G = nx.complete_graph(9) + G.add_node(9) + vpos = nx.random_layout(G, scale=2, center=(4,5)) + self.check_scale_and_center(vpos, scale=2, center=(4,5)) + vpos = nx.spring_layout(G, scale=2, center=(4,5)) + self.check_scale_and_center(vpos, scale=2, center=(4,5)) + vpos = nx.spectral_layout(G, scale=2, center=(4,5)) + self.check_scale_and_center(vpos, scale=2, center=(4,5)) + # circular can have twice as big length + vpos = nx.circular_layout(G, scale=2, center=(4,5)) + self.check_scale_and_center(vpos, scale=2*2, center=(4,5)) + vpos = nx.shell_layout(G, scale=2, center=(4,5)) + self.check_scale_and_center(vpos, scale=2*2, center=(4,5)) + + # check default center and scale + vpos = nx.random_layout(G) + self.check_scale_and_center(vpos, scale=1, center=(0.5,0.5)) + vpos = nx.spring_layout(G) + self.check_scale_and_center(vpos, scale=1, center=(0.5,0.5)) + vpos = nx.spectral_layout(G) + self.check_scale_and_center(vpos, scale=1, center=(0.5,0.5)) + vpos = nx.circular_layout(G) + self.check_scale_and_center(vpos, scale=2, center=(0,0)) + vpos = nx.shell_layout(G) + self.check_scale_and_center(vpos, scale=2, center=(0,0)) + + def test_shell_layout(self): + G = nx.complete_graph(9) + shells=[[0], [1,2,3,5], [4,6,7,8]] + vpos = nx.shell_layout(G, nlist=shells) + vpos = nx.shell_layout(G, nlist=shells, scale=2, center=(3,4)) + shells=[[0,1,2,3,5], [4,6,7,8]] + vpos = nx.shell_layout(G, nlist=shells) + vpos = nx.shell_layout(G, nlist=shells, scale=2, center=(3,4)) + + def test_spring_args(self): + G = nx.complete_graph(9) + vpos = nx.spring_layout(G, dim=3) + assert_equal(vpos[0].shape, (3,)) + vpos = nx.spring_layout(G, fixed=[0,1], pos={1:(0,0), 2:(1,1)}) + vpos = nx.spring_layout(G, k=2, fixed=[0,1], pos={1:(0,0), 2:(1,1)}) + vpos = nx.spring_layout(G, scale=3, center=(2,5)) + vpos = nx.spring_layout(G, scale=3) + vpos = nx.spring_layout(G, center=(2,5)) + def test_spectral_for_small_graphs(self): + G = nx.Graph() + vpos = nx.spectral_layout(G) + vpos = nx.spectral_layout(G, center=(2,3)) + G.add_node(0) + vpos = nx.spectral_layout(G) + vpos = nx.spectral_layout(G, center=(2,3)) + G.add_node(1) + vpos = nx.spectral_layout(G) + vpos = nx.spectral_layout(G, center=(2,3)) + # 3 nodes should allow eigensolvers to work + G.add_node(2) + vpos = nx.spectral_layout(G) + vpos = nx.spectral_layout(G, center=(2,3)) def test_adjacency_interface_numpy(self): A=nx.to_numpy_matrix(self.Gs) |