layout.py 65 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036
  1. """
  2. ******
  3. Layout
  4. ******
  5. Node positioning algorithms for graph drawing.
  6. For `random_layout()` the possible resulting shape
  7. is a square of side [0, scale] (default: [0, 1])
  8. Changing `center` shifts the layout by that amount.
  9. For the other layout routines, the extent is
  10. [center - scale, center + scale] (default: [-1, 1]).
  11. Warning: Most layout routines have only been tested in 2-dimensions.
  12. """
  13. import networkx as nx
  14. from networkx.utils import np_random_state
  15. __all__ = [
  16. "bipartite_layout",
  17. "circular_layout",
  18. "forceatlas2_layout",
  19. "kamada_kawai_layout",
  20. "random_layout",
  21. "rescale_layout",
  22. "rescale_layout_dict",
  23. "shell_layout",
  24. "spring_layout",
  25. "spectral_layout",
  26. "planar_layout",
  27. "fruchterman_reingold_layout",
  28. "spiral_layout",
  29. "multipartite_layout",
  30. "bfs_layout",
  31. "arf_layout",
  32. ]
  33. def _process_params(G, center, dim):
  34. # Some boilerplate code.
  35. import numpy as np
  36. if not isinstance(G, nx.Graph):
  37. empty_graph = nx.Graph()
  38. empty_graph.add_nodes_from(G)
  39. G = empty_graph
  40. if center is None:
  41. center = np.zeros(dim)
  42. else:
  43. center = np.asarray(center)
  44. if len(center) != dim:
  45. msg = "length of center coordinates must match dimension of layout"
  46. raise ValueError(msg)
  47. return G, center
  48. @np_random_state(3)
  49. def random_layout(G, center=None, dim=2, seed=None, store_pos_as=None):
  50. """Position nodes uniformly at random in the unit square.
  51. For every node, a position is generated by choosing each of dim
  52. coordinates uniformly at random on the interval [0.0, 1.0).
  53. NumPy (http://scipy.org) is required for this function.
  54. Parameters
  55. ----------
  56. G : NetworkX graph or list of nodes
  57. A position will be assigned to every node in G.
  58. center : array-like or None
  59. Coordinate pair around which to center the layout.
  60. dim : int
  61. Dimension of layout.
  62. seed : int, RandomState instance or None optional (default=None)
  63. Set the random state for deterministic node layouts.
  64. If int, `seed` is the seed used by the random number generator,
  65. if numpy.random.RandomState instance, `seed` is the random
  66. number generator,
  67. if None, the random number generator is the RandomState instance used
  68. by numpy.random.
  69. store_pos_as : str, default None
  70. If non-None, the position of each node will be stored on the graph as
  71. an attribute with this string as its name, which can be accessed with
  72. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  73. Returns
  74. -------
  75. pos : dict
  76. A dictionary of positions keyed by node
  77. Examples
  78. --------
  79. >>> from pprint import pprint
  80. >>> G = nx.lollipop_graph(4, 3)
  81. >>> pos = nx.random_layout(G)
  82. >>> # suppress the returned dict and store on the graph directly
  83. >>> _ = nx.random_layout(G, seed=42, store_pos_as="pos")
  84. >>> pprint(nx.get_node_attributes(G, "pos"))
  85. {0: array([0.37454012, 0.9507143 ], dtype=float32),
  86. 1: array([0.7319939, 0.5986585], dtype=float32),
  87. 2: array([0.15601864, 0.15599452], dtype=float32),
  88. 3: array([0.05808361, 0.8661761 ], dtype=float32),
  89. 4: array([0.601115 , 0.7080726], dtype=float32),
  90. 5: array([0.02058449, 0.96990985], dtype=float32),
  91. 6: array([0.83244264, 0.21233912], dtype=float32)}
  92. """
  93. import numpy as np
  94. G, center = _process_params(G, center, dim)
  95. pos = seed.rand(len(G), dim) + center
  96. pos = pos.astype(np.float32)
  97. pos = dict(zip(G, pos))
  98. if store_pos_as is not None:
  99. nx.set_node_attributes(G, pos, store_pos_as)
  100. return pos
  101. def circular_layout(G, scale=1, center=None, dim=2, store_pos_as=None):
  102. # dim=2 only
  103. """Position nodes on a circle.
  104. Parameters
  105. ----------
  106. G : NetworkX graph or list of nodes
  107. A position will be assigned to every node in G.
  108. scale : number (default: 1)
  109. Scale factor for positions.
  110. center : array-like or None
  111. Coordinate pair around which to center the layout.
  112. dim : int
  113. Dimension of layout.
  114. If dim>2, the remaining dimensions are set to zero
  115. in the returned positions.
  116. If dim<2, a ValueError is raised.
  117. store_pos_as : str, default None
  118. If non-None, the position of each node will be stored on the graph as
  119. an attribute with this string as its name, which can be accessed with
  120. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  121. Returns
  122. -------
  123. pos : dict
  124. A dictionary of positions keyed by node
  125. Raises
  126. ------
  127. ValueError
  128. If dim < 2
  129. Examples
  130. --------
  131. >>> from pprint import pprint
  132. >>> G = nx.path_graph(4)
  133. >>> pos = nx.circular_layout(G)
  134. >>> # suppress the returned dict and store on the graph directly
  135. >>> _ = nx.circular_layout(G, store_pos_as="pos")
  136. >>> pprint(nx.get_node_attributes(G, "pos"))
  137. {0: array([9.99999986e-01, 2.18556937e-08]),
  138. 1: array([-3.57647606e-08, 1.00000000e+00]),
  139. 2: array([-9.9999997e-01, -6.5567081e-08]),
  140. 3: array([ 1.98715071e-08, -9.99999956e-01])}
  141. Notes
  142. -----
  143. This algorithm currently only works in two dimensions and does not
  144. try to minimize edge crossings.
  145. """
  146. import numpy as np
  147. if dim < 2:
  148. raise ValueError("cannot handle dimensions < 2")
  149. G, center = _process_params(G, center, dim)
  150. paddims = max(0, (dim - 2))
  151. if len(G) == 0:
  152. pos = {}
  153. elif len(G) == 1:
  154. pos = {nx.utils.arbitrary_element(G): center}
  155. else:
  156. # Discard the extra angle since it matches 0 radians.
  157. theta = np.linspace(0, 1, len(G) + 1)[:-1] * 2 * np.pi
  158. theta = theta.astype(np.float32)
  159. pos = np.column_stack(
  160. [np.cos(theta), np.sin(theta), np.zeros((len(G), paddims))]
  161. )
  162. pos = rescale_layout(pos, scale=scale) + center
  163. pos = dict(zip(G, pos))
  164. if store_pos_as is not None:
  165. nx.set_node_attributes(G, pos, store_pos_as)
  166. return pos
  167. def shell_layout(
  168. G, nlist=None, rotate=None, scale=1, center=None, dim=2, store_pos_as=None
  169. ):
  170. """Position nodes in concentric circles.
  171. Parameters
  172. ----------
  173. G : NetworkX graph or list of nodes
  174. A position will be assigned to every node in G.
  175. nlist : list of lists
  176. List of node lists for each shell.
  177. rotate : angle in radians (default=pi/len(nlist))
  178. Angle by which to rotate the starting position of each shell
  179. relative to the starting position of the previous shell.
  180. To recreate behavior before v2.5 use rotate=0.
  181. scale : number (default: 1)
  182. Scale factor for positions.
  183. center : array-like or None
  184. Coordinate pair around which to center the layout.
  185. dim : int
  186. Dimension of layout, currently only dim=2 is supported.
  187. Other dimension values result in a ValueError.
  188. store_pos_as : str, default None
  189. If non-None, the position of each node will be stored on the graph as
  190. an attribute with this string as its name, which can be accessed with
  191. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  192. Returns
  193. -------
  194. pos : dict
  195. A dictionary of positions keyed by node
  196. Raises
  197. ------
  198. ValueError
  199. If dim != 2
  200. Examples
  201. --------
  202. >>> from pprint import pprint
  203. >>> G = nx.path_graph(4)
  204. >>> shells = [[0], [1, 2, 3]]
  205. >>> pos = nx.shell_layout(G, shells)
  206. >>> # suppress the returned dict and store on the graph directly
  207. >>> _ = nx.shell_layout(G, shells, store_pos_as="pos")
  208. >>> pprint(nx.get_node_attributes(G, "pos"))
  209. {0: array([0., 0.]),
  210. 1: array([-5.00000000e-01, -4.37113883e-08]),
  211. 2: array([ 0.24999996, -0.43301272]),
  212. 3: array([0.24999981, 0.43301281])}
  213. Notes
  214. -----
  215. This algorithm currently only works in two dimensions and does not
  216. try to minimize edge crossings.
  217. """
  218. import numpy as np
  219. if dim != 2:
  220. raise ValueError("can only handle 2 dimensions")
  221. G, center = _process_params(G, center, dim)
  222. if len(G) == 0:
  223. return {}
  224. if len(G) == 1:
  225. return {nx.utils.arbitrary_element(G): center}
  226. if nlist is None:
  227. # draw the whole graph in one shell
  228. nlist = [list(G)]
  229. radius_bump = scale / len(nlist)
  230. if len(nlist[0]) == 1:
  231. # single node at center
  232. radius = 0.0
  233. else:
  234. # else start at r=1
  235. radius = radius_bump
  236. if rotate is None:
  237. rotate = np.pi / len(nlist)
  238. first_theta = rotate
  239. npos = {}
  240. for nodes in nlist:
  241. # Discard the last angle (endpoint=False) since 2*pi matches 0 radians
  242. theta = (
  243. np.linspace(0, 2 * np.pi, len(nodes), endpoint=False, dtype=np.float32)
  244. + first_theta
  245. )
  246. pos = radius * np.column_stack([np.cos(theta), np.sin(theta)]) + center
  247. npos.update(zip(nodes, pos))
  248. radius += radius_bump
  249. first_theta += rotate
  250. if store_pos_as is not None:
  251. nx.set_node_attributes(G, npos, store_pos_as)
  252. return npos
  253. def bipartite_layout(
  254. G,
  255. nodes=None,
  256. align="vertical",
  257. scale=1,
  258. center=None,
  259. aspect_ratio=4 / 3,
  260. store_pos_as=None,
  261. ):
  262. """Position nodes in two straight lines.
  263. Parameters
  264. ----------
  265. G : NetworkX graph or list of nodes
  266. A position will be assigned to every node in G.
  267. nodes : collection of nodes
  268. Nodes in one node set of the graph. This set will be placed on
  269. left or top. If `None` (the default), a node set is chosen arbitrarily
  270. if the graph if bipartite.
  271. align : string (default='vertical')
  272. The alignment of nodes. Vertical or horizontal.
  273. scale : number (default: 1)
  274. Scale factor for positions.
  275. center : array-like or None
  276. Coordinate pair around which to center the layout.
  277. aspect_ratio : number (default=4/3):
  278. The ratio of the width to the height of the layout.
  279. store_pos_as : str, default None
  280. If non-None, the position of each node will be stored on the graph as
  281. an attribute with this string as its name, which can be accessed with
  282. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  283. Returns
  284. -------
  285. pos : dict
  286. A dictionary of positions keyed by node.
  287. Raises
  288. ------
  289. NetworkXError
  290. If ``nodes=None`` and `G` is not bipartite.
  291. Examples
  292. --------
  293. >>> G = nx.complete_bipartite_graph(3, 3)
  294. >>> pos = nx.bipartite_layout(G)
  295. The ordering of the layout (i.e. which nodes appear on the left/top) can
  296. be specified with the `nodes` parameter:
  297. >>> top, bottom = nx.bipartite.sets(G)
  298. >>> pos = nx.bipartite_layout(G, nodes=bottom) # "bottom" set appears on the left
  299. `store_pos_as` can be used to store the node positions for the computed layout
  300. directly on the nodes:
  301. >>> _ = nx.bipartite_layout(G, nodes=bottom, store_pos_as="pos")
  302. >>> from pprint import pprint
  303. >>> pprint(nx.get_node_attributes(G, "pos"))
  304. {0: array([ 1. , -0.75]),
  305. 1: array([1., 0.]),
  306. 2: array([1. , 0.75]),
  307. 3: array([-1. , -0.75]),
  308. 4: array([-1., 0.]),
  309. 5: array([-1. , 0.75])}
  310. The ``bipartite_layout`` function can be used with non-bipartite graphs
  311. by explicitly specifying how the layout should be partitioned with `nodes`:
  312. >>> G = nx.complete_graph(5) # Non-bipartite
  313. >>> pos = nx.bipartite_layout(G, nodes={0, 1, 2})
  314. Notes
  315. -----
  316. This algorithm currently only works in two dimensions and does not
  317. try to minimize edge crossings.
  318. """
  319. import numpy as np
  320. if align not in ("vertical", "horizontal"):
  321. msg = "align must be either vertical or horizontal."
  322. raise ValueError(msg)
  323. G, center = _process_params(G, center=center, dim=2)
  324. if len(G) == 0:
  325. return {}
  326. height = 1
  327. width = aspect_ratio * height
  328. offset = (width / 2, height / 2)
  329. if nodes is None:
  330. top, bottom = nx.bipartite.sets(G)
  331. nodes = list(G)
  332. else:
  333. top = set(nodes)
  334. bottom = set(G) - top
  335. # Preserves backward-compatible node ordering in returned pos dict
  336. nodes = list(top) + list(bottom)
  337. left_xs = np.repeat(0, len(top))
  338. right_xs = np.repeat(width, len(bottom))
  339. left_ys = np.linspace(0, height, len(top))
  340. right_ys = np.linspace(0, height, len(bottom))
  341. top_pos = np.column_stack([left_xs, left_ys]) - offset
  342. bottom_pos = np.column_stack([right_xs, right_ys]) - offset
  343. pos = np.concatenate([top_pos, bottom_pos])
  344. pos = rescale_layout(pos, scale=scale) + center
  345. if align == "horizontal":
  346. pos = pos[:, ::-1] # swap x and y coords
  347. pos = dict(zip(nodes, pos))
  348. if store_pos_as is not None:
  349. nx.set_node_attributes(G, pos, store_pos_as)
  350. return pos
  351. @np_random_state("seed")
  352. def spring_layout(
  353. G,
  354. k=None,
  355. pos=None,
  356. fixed=None,
  357. iterations=50,
  358. threshold=1e-4,
  359. weight="weight",
  360. scale=1,
  361. center=None,
  362. dim=2,
  363. seed=None,
  364. store_pos_as=None,
  365. *,
  366. method="auto",
  367. gravity=1.0,
  368. ):
  369. """Position nodes using Fruchterman-Reingold force-directed algorithm.
  370. The algorithm simulates a force-directed representation of the network
  371. treating edges as springs holding nodes close, while treating nodes
  372. as repelling objects, sometimes called an anti-gravity force.
  373. Simulation continues until the positions are close to an equilibrium.
  374. There are some hard-coded values: minimal distance between
  375. nodes (0.01) and "temperature" of 0.1 to ensure nodes don't fly away.
  376. During the simulation, `k` helps determine the distance between nodes,
  377. though `scale` and `center` determine the size and place after
  378. rescaling occurs at the end of the simulation.
  379. Fixing some nodes doesn't allow them to move in the simulation.
  380. It also turns off the rescaling feature at the simulation's end.
  381. In addition, setting `scale` to `None` turns off rescaling.
  382. Parameters
  383. ----------
  384. G : NetworkX graph or list of nodes
  385. A position will be assigned to every node in G.
  386. k : float (default=None)
  387. Optimal distance between nodes. If None the distance is set to
  388. 1/sqrt(n) where n is the number of nodes. Increase this value
  389. to move nodes farther apart.
  390. pos : dict or None optional (default=None)
  391. Initial positions for nodes as a dictionary with node as keys
  392. and values as a coordinate list or tuple. If None, then use
  393. random initial positions.
  394. fixed : list or None optional (default=None)
  395. Nodes to keep fixed at initial position.
  396. Nodes not in ``G.nodes`` are ignored.
  397. ValueError raised if `fixed` specified and `pos` not.
  398. iterations : int optional (default=50)
  399. Maximum number of iterations taken
  400. threshold: float optional (default = 1e-4)
  401. Threshold for relative error in node position changes.
  402. The iteration stops if the error is below this threshold.
  403. weight : string or None optional (default='weight')
  404. The edge attribute that holds the numerical value used for
  405. the edge weight. Larger means a stronger attractive force.
  406. If None, then all edge weights are 1.
  407. scale : number or None (default: 1)
  408. Scale factor for positions. Not used unless `fixed is None`.
  409. If scale is None, no rescaling is performed.
  410. center : array-like or None
  411. Coordinate pair around which to center the layout.
  412. Not used unless `fixed is None`.
  413. dim : int
  414. Dimension of layout.
  415. seed : int, RandomState instance or None optional (default=None)
  416. Used only for the initial positions in the algorithm.
  417. Set the random state for deterministic node layouts.
  418. If int, `seed` is the seed used by the random number generator,
  419. if numpy.random.RandomState instance, `seed` is the random
  420. number generator,
  421. if None, the random number generator is the RandomState instance used
  422. by numpy.random.
  423. store_pos_as : str, default None
  424. If non-None, the position of each node will be stored on the graph as
  425. an attribute with this string as its name, which can be accessed with
  426. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  427. method : str optional (default='auto')
  428. The method to compute the layout.
  429. If 'force', the force-directed Fruchterman-Reingold algorithm [1]_ is used.
  430. If 'energy', the energy-based optimization algorithm [2]_ is used with absolute
  431. values of edge weights and gravitational forces acting on each connected component.
  432. If 'auto', we use 'force' if ``len(G) < 500`` and 'energy' otherwise.
  433. gravity: float optional (default=1.0)
  434. Used only for the method='energy'.
  435. The positive coefficient of gravitational forces per connected component.
  436. Returns
  437. -------
  438. pos : dict
  439. A dictionary of positions keyed by node
  440. Examples
  441. --------
  442. >>> from pprint import pprint
  443. >>> G = nx.path_graph(4)
  444. >>> pos = nx.spring_layout(G)
  445. >>> # suppress the returned dict and store on the graph directly
  446. >>> _ = nx.spring_layout(G, seed=123, store_pos_as="pos")
  447. >>> pprint(nx.get_node_attributes(G, "pos"))
  448. {0: array([-0.61495802, -1. ]),
  449. 1: array([-0.21789544, -0.35432583]),
  450. 2: array([0.21847843, 0.35527369]),
  451. 3: array([0.61437502, 0.99905215])}
  452. # The same using longer but equivalent function name
  453. >>> pos = nx.fruchterman_reingold_layout(G)
  454. References
  455. ----------
  456. .. [1] Fruchterman, Thomas MJ, and Edward M. Reingold.
  457. "Graph drawing by force-directed placement."
  458. Software: Practice and experience 21, no. 11 (1991): 1129-1164.
  459. http://dx.doi.org/10.1002/spe.4380211102
  460. .. [2] Hamaguchi, Hiroki, Naoki Marumo, and Akiko Takeda.
  461. "Initial Placement for Fruchterman--Reingold Force Model With Coordinate Newton Direction."
  462. arXiv preprint arXiv:2412.20317 (2024).
  463. https://arxiv.org/abs/2412.20317
  464. """
  465. import numpy as np
  466. if method not in ("auto", "force", "energy"):
  467. raise ValueError("the method must be either auto, force, or energy.")
  468. if method == "auto":
  469. method = "force" if len(G) < 500 else "energy"
  470. G, center = _process_params(G, center, dim)
  471. if fixed is not None:
  472. if pos is None:
  473. raise ValueError("nodes are fixed without positions given")
  474. for node in fixed:
  475. if node not in pos:
  476. raise ValueError("nodes are fixed without positions given")
  477. nfixed = {node: i for i, node in enumerate(G)}
  478. fixed = np.asarray([nfixed[node] for node in fixed if node in nfixed])
  479. if pos is not None:
  480. # Determine size of existing domain to adjust initial positions
  481. dom_size = max(coord for pos_tup in pos.values() for coord in pos_tup)
  482. if dom_size == 0:
  483. dom_size = 1
  484. pos_arr = seed.rand(len(G), dim) * dom_size + center
  485. for i, n in enumerate(G):
  486. if n in pos:
  487. pos_arr[i] = np.asarray(pos[n])
  488. else:
  489. pos_arr = None
  490. dom_size = 1
  491. if len(G) == 0:
  492. return {}
  493. if len(G) == 1:
  494. pos = {nx.utils.arbitrary_element(G.nodes()): center}
  495. if store_pos_as is not None:
  496. nx.set_node_attributes(G, pos, store_pos_as)
  497. return pos
  498. # Sparse matrix
  499. if len(G) >= 500 or method == "energy":
  500. A = nx.to_scipy_sparse_array(G, weight=weight, dtype="f")
  501. if k is None and fixed is not None:
  502. # We must adjust k by domain size for layouts not near 1x1
  503. nnodes, _ = A.shape
  504. k = dom_size / np.sqrt(nnodes)
  505. pos = _sparse_fruchterman_reingold(
  506. A, k, pos_arr, fixed, iterations, threshold, dim, seed, method, gravity
  507. )
  508. else:
  509. A = nx.to_numpy_array(G, weight=weight)
  510. if k is None and fixed is not None:
  511. # We must adjust k by domain size for layouts not near 1x1
  512. nnodes, _ = A.shape
  513. k = dom_size / np.sqrt(nnodes)
  514. pos = _fruchterman_reingold(
  515. A, k, pos_arr, fixed, iterations, threshold, dim, seed
  516. )
  517. if fixed is None and scale is not None:
  518. pos = rescale_layout(pos, scale=scale) + center
  519. pos = dict(zip(G, pos))
  520. if store_pos_as is not None:
  521. nx.set_node_attributes(G, pos, store_pos_as)
  522. return pos
  523. fruchterman_reingold_layout = spring_layout
  524. @np_random_state(7)
  525. def _fruchterman_reingold(
  526. A, k=None, pos=None, fixed=None, iterations=50, threshold=1e-4, dim=2, seed=None
  527. ):
  528. # Position nodes in adjacency matrix A using Fruchterman-Reingold
  529. # Entry point for NetworkX graph is fruchterman_reingold_layout()
  530. import numpy as np
  531. try:
  532. nnodes, _ = A.shape
  533. except AttributeError as err:
  534. msg = "fruchterman_reingold() takes an adjacency matrix as input"
  535. raise nx.NetworkXError(msg) from err
  536. if pos is None:
  537. # random initial positions
  538. pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
  539. else:
  540. # make sure positions are of same type as matrix
  541. pos = pos.astype(A.dtype)
  542. # optimal distance between nodes
  543. if k is None:
  544. k = np.sqrt(1.0 / nnodes)
  545. # the initial "temperature" is about .1 of domain area (=1x1)
  546. # this is the largest step allowed in the dynamics.
  547. # We need to calculate this in case our fixed positions force our domain
  548. # to be much bigger than 1x1
  549. t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
  550. # simple cooling scheme.
  551. # linearly step down by dt on each iteration so last iteration is size dt.
  552. dt = t / (iterations + 1)
  553. delta = np.zeros((pos.shape[0], pos.shape[0], pos.shape[1]), dtype=A.dtype)
  554. # the inscrutable (but fast) version
  555. # this is still O(V^2)
  556. # could use multilevel methods to speed this up significantly
  557. for iteration in range(iterations):
  558. # matrix of difference between points
  559. delta = pos[:, np.newaxis, :] - pos[np.newaxis, :, :]
  560. # distance between points
  561. distance = np.linalg.norm(delta, axis=-1)
  562. # enforce minimum distance of 0.01
  563. np.clip(distance, 0.01, None, out=distance)
  564. # displacement "force"
  565. displacement = np.einsum(
  566. "ijk,ij->ik", delta, (k * k / distance**2 - A * distance / k)
  567. )
  568. # update positions
  569. length = np.linalg.norm(displacement, axis=-1)
  570. # Threshold the minimum length prior to position scaling
  571. # See gh-8113 for detailed discussion of the threshold
  572. length = np.clip(length, a_min=0.01, a_max=None)
  573. delta_pos = np.einsum("ij,i->ij", displacement, t / length)
  574. if fixed is not None:
  575. # don't change positions of fixed nodes
  576. delta_pos[fixed] = 0.0
  577. pos += delta_pos
  578. # cool temperature
  579. t -= dt
  580. if (np.linalg.norm(delta_pos) / nnodes) < threshold:
  581. break
  582. return pos
  583. @np_random_state(7)
  584. def _sparse_fruchterman_reingold(
  585. A,
  586. k=None,
  587. pos=None,
  588. fixed=None,
  589. iterations=50,
  590. threshold=1e-4,
  591. dim=2,
  592. seed=None,
  593. method="energy",
  594. gravity=1.0,
  595. ):
  596. # Position nodes in adjacency matrix A using Fruchterman-Reingold
  597. # Entry point for NetworkX graph is fruchterman_reingold_layout()
  598. # Sparse version
  599. import numpy as np
  600. import scipy as sp
  601. try:
  602. nnodes, _ = A.shape
  603. except AttributeError as err:
  604. msg = "fruchterman_reingold() takes an adjacency matrix as input"
  605. raise nx.NetworkXError(msg) from err
  606. if pos is None:
  607. # random initial positions
  608. pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
  609. else:
  610. # make sure positions are of same type as matrix
  611. pos = pos.astype(A.dtype)
  612. # no fixed nodes
  613. if fixed is None:
  614. fixed = []
  615. # optimal distance between nodes
  616. if k is None:
  617. k = np.sqrt(1.0 / nnodes)
  618. if method == "energy":
  619. return _energy_fruchterman_reingold(
  620. A, nnodes, k, pos, fixed, iterations, threshold, dim, gravity
  621. )
  622. # make sure we have a LIst of Lists representation
  623. try:
  624. A = A.tolil()
  625. except AttributeError:
  626. A = (sp.sparse.coo_array(A)).tolil()
  627. # the initial "temperature" is about .1 of domain area (=1x1)
  628. # this is the largest step allowed in the dynamics.
  629. t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
  630. # simple cooling scheme.
  631. # linearly step down by dt on each iteration so last iteration is size dt.
  632. dt = t / (iterations + 1)
  633. displacement = np.zeros((dim, nnodes))
  634. for iteration in range(iterations):
  635. displacement *= 0
  636. # loop over rows
  637. for i in range(A.shape[0]):
  638. if i in fixed:
  639. continue
  640. # difference between this row's node position and all others
  641. delta = (pos[i] - pos).T
  642. # distance between points
  643. distance = np.sqrt((delta**2).sum(axis=0))
  644. # enforce minimum distance of 0.01
  645. distance = np.clip(distance, a_min=0.01, a_max=None)
  646. # the adjacency matrix row
  647. Ai = A.getrowview(i).toarray() # TODO: revisit w/ sparse 1D container
  648. # displacement "force"
  649. displacement[:, i] += (
  650. delta * (k * k / distance**2 - Ai * distance / k)
  651. ).sum(axis=1)
  652. # update positions
  653. length = np.sqrt((displacement**2).sum(axis=0))
  654. # Threshold the minimum length prior to position scaling
  655. # See gh-8113 for detailed discussion of the threshold
  656. length = np.clip(length, a_min=0.01, a_max=None)
  657. delta_pos = (displacement * t / length).T
  658. pos += delta_pos
  659. # cool temperature
  660. t -= dt
  661. if (np.linalg.norm(delta_pos) / nnodes) < threshold:
  662. break
  663. return pos
  664. def _energy_fruchterman_reingold(
  665. A, nnodes, k, pos, fixed, iterations, threshold, dim, gravity
  666. ):
  667. # Entry point for NetworkX graph is fruchterman_reingold_layout()
  668. # energy-based version
  669. import numpy as np
  670. import scipy as sp
  671. if gravity <= 0:
  672. raise ValueError(f"the gravity must be positive.")
  673. # make sure we have a Compressed Sparse Row format
  674. try:
  675. A = A.tocsr()
  676. except AttributeError:
  677. A = sp.sparse.csr_array(A)
  678. # Take absolute values of edge weights and symmetrize it
  679. A = np.abs(A)
  680. A = (A + A.T) / 2
  681. n_components, labels = sp.sparse.csgraph.connected_components(A, directed=False)
  682. bincount = np.bincount(labels)
  683. batchsize = 500
  684. def _cost_FR(x):
  685. pos = x.reshape((nnodes, dim))
  686. grad = np.zeros((nnodes, dim))
  687. cost = 0.0
  688. for l in range(0, nnodes, batchsize):
  689. r = min(l + batchsize, nnodes)
  690. # difference between selected node positions and all others
  691. delta = pos[l:r, np.newaxis, :] - pos[np.newaxis, :, :]
  692. # distance between points with a minimum distance of 1e-5
  693. distance2 = np.sum(delta * delta, axis=2)
  694. distance2 = np.maximum(distance2, 1e-10)
  695. distance = np.sqrt(distance2)
  696. # temporary variable for calculation
  697. Ad = A[l:r] * distance
  698. # attractive forces and repulsive forces
  699. grad[l:r] = 2 * np.einsum("ij,ijk->ik", Ad / k - k**2 / distance2, delta)
  700. # integrated attractive forces
  701. cost += np.sum(Ad * distance2) / (3 * k)
  702. # integrated repulsive forces
  703. cost -= k**2 * np.sum(np.log(distance))
  704. # gravitational force from the centroids of connected components to (0.5, ..., 0.5)^T
  705. centers = np.zeros((n_components, dim))
  706. np.add.at(centers, labels, pos)
  707. delta0 = centers / bincount[:, np.newaxis] - 0.5
  708. grad += gravity * delta0[labels]
  709. cost += gravity * 0.5 * np.sum(bincount * np.linalg.norm(delta0, axis=1) ** 2)
  710. # fix positions of fixed nodes
  711. grad[fixed] = 0.0
  712. return cost, grad.ravel()
  713. # Optimization of the energy function by L-BFGS algorithm
  714. options = {"maxiter": iterations, "gtol": threshold}
  715. return sp.optimize.minimize(
  716. _cost_FR, pos.ravel(), method="L-BFGS-B", jac=True, options=options
  717. ).x.reshape((nnodes, dim))
  718. def kamada_kawai_layout(
  719. G,
  720. dist=None,
  721. pos=None,
  722. weight="weight",
  723. scale=1,
  724. center=None,
  725. dim=2,
  726. store_pos_as=None,
  727. ):
  728. """Position nodes using Kamada-Kawai path-length cost-function.
  729. Parameters
  730. ----------
  731. G : NetworkX graph or list of nodes
  732. A position will be assigned to every node in G.
  733. dist : dict (default=None)
  734. A two-level dictionary of optimal distances between nodes,
  735. indexed by source and destination node.
  736. If None, the distance is computed using shortest_path_length().
  737. pos : dict or None optional (default=None)
  738. Initial positions for nodes as a dictionary with node as keys
  739. and values as a coordinate list or tuple. If None, then use
  740. circular_layout() for dim >= 2 and a linear layout for dim == 1.
  741. weight : string or None optional (default='weight')
  742. The edge attribute that holds the numerical value used for
  743. the edge weight. If None, then all edge weights are 1.
  744. scale : number (default: 1)
  745. Scale factor for positions.
  746. center : array-like or None
  747. Coordinate pair around which to center the layout.
  748. dim : int
  749. Dimension of layout.
  750. store_pos_as : str, default None
  751. If non-None, the position of each node will be stored on the graph as
  752. an attribute with this string as its name, which can be accessed with
  753. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  754. Returns
  755. -------
  756. pos : dict
  757. A dictionary of positions keyed by node
  758. Examples
  759. --------
  760. >>> from pprint import pprint
  761. >>> G = nx.path_graph(4)
  762. >>> pos = nx.kamada_kawai_layout(G)
  763. >>> # suppress the returned dict and store on the graph directly
  764. >>> _ = nx.kamada_kawai_layout(G, store_pos_as="pos")
  765. >>> pprint(nx.get_node_attributes(G, "pos"))
  766. {0: array([0.99996577, 0.99366857]),
  767. 1: array([0.32913544, 0.33543827]),
  768. 2: array([-0.33544334, -0.32910684]),
  769. 3: array([-0.99365787, -1. ])}
  770. """
  771. import numpy as np
  772. G, center = _process_params(G, center, dim)
  773. nNodes = len(G)
  774. if nNodes == 0:
  775. return {}
  776. if dist is None:
  777. dist = dict(nx.shortest_path_length(G, weight=weight))
  778. dist_mtx = 1e6 * np.ones((nNodes, nNodes))
  779. for row, nr in enumerate(G):
  780. if nr not in dist:
  781. continue
  782. rdist = dist[nr]
  783. for col, nc in enumerate(G):
  784. if nc not in rdist:
  785. continue
  786. dist_mtx[row][col] = rdist[nc]
  787. if pos is None:
  788. if dim >= 3:
  789. pos = random_layout(G, dim=dim)
  790. elif dim == 2:
  791. pos = circular_layout(G, dim=dim)
  792. else:
  793. pos = dict(zip(G, np.linspace(0, 1, len(G))))
  794. pos_arr = np.array([pos[n] for n in G])
  795. pos = _kamada_kawai_solve(dist_mtx, pos_arr, dim)
  796. pos = rescale_layout(pos, scale=scale) + center
  797. pos = dict(zip(G, pos))
  798. if store_pos_as is not None:
  799. nx.set_node_attributes(G, pos, store_pos_as)
  800. return pos
  801. def _kamada_kawai_solve(dist_mtx, pos_arr, dim):
  802. # Anneal node locations based on the Kamada-Kawai cost-function,
  803. # using the supplied matrix of preferred inter-node distances,
  804. # and starting locations.
  805. import numpy as np
  806. import scipy as sp
  807. meanwt = 1e-3
  808. costargs = (np, 1 / (dist_mtx + np.eye(dist_mtx.shape[0]) * 1e-3), meanwt, dim)
  809. optresult = sp.optimize.minimize(
  810. _kamada_kawai_costfn,
  811. pos_arr.ravel(),
  812. method="L-BFGS-B",
  813. args=costargs,
  814. jac=True,
  815. )
  816. return optresult.x.reshape((-1, dim))
  817. def _kamada_kawai_costfn(pos_vec, np, invdist, meanweight, dim):
  818. # Cost-function and gradient for Kamada-Kawai layout algorithm
  819. nNodes = invdist.shape[0]
  820. pos_arr = pos_vec.reshape((nNodes, dim))
  821. delta = pos_arr[:, np.newaxis, :] - pos_arr[np.newaxis, :, :]
  822. nodesep = np.linalg.norm(delta, axis=-1)
  823. direction = np.einsum("ijk,ij->ijk", delta, 1 / (nodesep + np.eye(nNodes) * 1e-3))
  824. offset = nodesep * invdist - 1.0
  825. offset[np.diag_indices(nNodes)] = 0
  826. cost = 0.5 * np.sum(offset**2)
  827. grad = np.einsum("ij,ij,ijk->ik", invdist, offset, direction) - np.einsum(
  828. "ij,ij,ijk->jk", invdist, offset, direction
  829. )
  830. # Additional parabolic term to encourage mean position to be near origin:
  831. sumpos = np.sum(pos_arr, axis=0)
  832. cost += 0.5 * meanweight * np.sum(sumpos**2)
  833. grad += meanweight * sumpos
  834. return (cost, grad.ravel())
  835. def spectral_layout(G, weight="weight", scale=1, center=None, dim=2, store_pos_as=None):
  836. """Position nodes using the eigenvectors of the graph Laplacian.
  837. Using the unnormalized Laplacian, the layout shows possible clusters of
  838. nodes which are an approximation of the ratio cut. If dim is the number of
  839. dimensions then the positions are the entries of the dim eigenvectors
  840. corresponding to the ascending eigenvalues starting from the second one.
  841. Parameters
  842. ----------
  843. G : NetworkX graph or list of nodes
  844. A position will be assigned to every node in G.
  845. weight : string or None optional (default='weight')
  846. The edge attribute that holds the numerical value used for
  847. the edge weight. If None, then all edge weights are 1.
  848. scale : number (default: 1)
  849. Scale factor for positions.
  850. center : array-like or None
  851. Coordinate pair around which to center the layout.
  852. dim : int
  853. Dimension of layout.
  854. store_pos_as : str, default None
  855. If non-None, the position of each node will be stored on the graph as
  856. an attribute with this string as its name, which can be accessed with
  857. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  858. Returns
  859. -------
  860. pos : dict
  861. A dictionary of positions keyed by node
  862. Examples
  863. --------
  864. >>> from pprint import pprint
  865. >>> G = nx.path_graph(4)
  866. >>> pos = nx.spectral_layout(G)
  867. >>> # suppress the returned dict and store on the graph directly
  868. >>> _ = nx.spectral_layout(G, store_pos_as="pos")
  869. >>> pprint(nx.get_node_attributes(G, "pos"))
  870. {0: array([-1. , 0.76536686]),
  871. 1: array([-0.41421356, -0.76536686]),
  872. 2: array([ 0.41421356, -0.76536686]),
  873. 3: array([1. , 0.76536686])}
  874. Notes
  875. -----
  876. Directed graphs will be considered as undirected graphs when
  877. positioning the nodes.
  878. For larger graphs (>500 nodes) this will use the SciPy sparse
  879. eigenvalue solver (ARPACK).
  880. """
  881. # handle some special cases that break the eigensolvers
  882. import numpy as np
  883. G, center = _process_params(G, center, dim)
  884. if len(G) <= 2:
  885. if len(G) == 0:
  886. pos = np.array([])
  887. elif len(G) == 1:
  888. pos = np.array([center])
  889. else:
  890. pos = np.array([np.zeros(dim), np.array(center) * 2.0])
  891. return dict(zip(G, pos))
  892. try:
  893. # Sparse matrix
  894. if len(G) < 500: # dense solver is faster for small graphs
  895. raise ValueError
  896. A = nx.to_scipy_sparse_array(G, weight=weight, dtype="d")
  897. # Symmetrize directed graphs
  898. if G.is_directed():
  899. A = A + np.transpose(A)
  900. pos = _sparse_spectral(A, dim)
  901. except (ImportError, ValueError):
  902. # Dense matrix
  903. A = nx.to_numpy_array(G, weight=weight)
  904. # Symmetrize directed graphs
  905. if G.is_directed():
  906. A += A.T
  907. pos = _spectral(A, dim)
  908. pos = rescale_layout(pos, scale=scale) + center
  909. pos = dict(zip(G, pos))
  910. if store_pos_as is not None:
  911. nx.set_node_attributes(G, pos, store_pos_as)
  912. return pos
  913. def _spectral(A, dim=2):
  914. # Input adjacency matrix A
  915. # Uses dense eigenvalue solver from numpy
  916. import numpy as np
  917. try:
  918. nnodes, _ = A.shape
  919. except AttributeError as err:
  920. msg = "spectral() takes an adjacency matrix as input"
  921. raise nx.NetworkXError(msg) from err
  922. # form Laplacian matrix where D is diagonal of degrees
  923. D = np.identity(nnodes, dtype=A.dtype) * np.sum(A, axis=1)
  924. L = D - A
  925. eigenvalues, eigenvectors = np.linalg.eig(L)
  926. # sort and keep smallest nonzero
  927. index = np.argsort(eigenvalues)[1 : dim + 1] # 0 index is zero eigenvalue
  928. return np.real(eigenvectors[:, index])
  929. def _sparse_spectral(A, dim=2):
  930. # Input adjacency matrix A
  931. # Uses sparse eigenvalue solver from scipy
  932. # Could use multilevel methods here, see Koren "On spectral graph drawing"
  933. import numpy as np
  934. import scipy as sp
  935. try:
  936. nnodes, _ = A.shape
  937. except AttributeError as err:
  938. msg = "sparse_spectral() takes an adjacency matrix as input"
  939. raise nx.NetworkXError(msg) from err
  940. # form Laplacian matrix
  941. D = sp.sparse.dia_array((A.sum(axis=1), 0), shape=(nnodes, nnodes)).tocsr()
  942. L = D - A
  943. k = dim + 1
  944. # number of Lanczos vectors for ARPACK solver.What is the right scaling?
  945. ncv = max(2 * k + 1, int(np.sqrt(nnodes)))
  946. # return smallest k eigenvalues and eigenvectors
  947. eigenvalues, eigenvectors = sp.sparse.linalg.eigsh(L, k, which="SM", ncv=ncv)
  948. index = np.argsort(eigenvalues)[1:k] # 0 index is zero eigenvalue
  949. return np.real(eigenvectors[:, index])
  950. def planar_layout(G, scale=1, center=None, dim=2, store_pos_as=None):
  951. """Position nodes without edge intersections.
  952. Parameters
  953. ----------
  954. G : NetworkX graph or list of nodes
  955. A position will be assigned to every node in G. If G is of type
  956. nx.PlanarEmbedding, the positions are selected accordingly.
  957. scale : number (default: 1)
  958. Scale factor for positions.
  959. center : array-like or None
  960. Coordinate pair around which to center the layout.
  961. dim : int
  962. Dimension of layout.
  963. store_pos_as : str, default None
  964. If non-None, the position of each node will be stored on the graph as
  965. an attribute with this string as its name, which can be accessed with
  966. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  967. Returns
  968. -------
  969. pos : dict
  970. A dictionary of positions keyed by node
  971. Raises
  972. ------
  973. NetworkXException
  974. If G is not planar
  975. Examples
  976. --------
  977. >>> from pprint import pprint
  978. >>> G = nx.path_graph(4)
  979. >>> pos = nx.planar_layout(G)
  980. >>> # suppress the returned dict and store on the graph directly
  981. >>> _ = nx.planar_layout(G, store_pos_as="pos")
  982. >>> pprint(nx.get_node_attributes(G, "pos"))
  983. {0: array([-0.77777778, -0.33333333]),
  984. 1: array([ 1. , -0.33333333]),
  985. 2: array([0.11111111, 0.55555556]),
  986. 3: array([-0.33333333, 0.11111111])}
  987. """
  988. import numpy as np
  989. if dim != 2:
  990. raise ValueError("can only handle 2 dimensions")
  991. G, center = _process_params(G, center, dim)
  992. if len(G) == 0:
  993. return {}
  994. if isinstance(G, nx.PlanarEmbedding):
  995. embedding = G
  996. else:
  997. is_planar, embedding = nx.check_planarity(G)
  998. if not is_planar:
  999. raise nx.NetworkXException("G is not planar.")
  1000. pos = nx.combinatorial_embedding_to_pos(embedding)
  1001. node_list = list(embedding)
  1002. pos = np.vstack([pos[x] for x in node_list])
  1003. pos = pos.astype(np.float64)
  1004. pos = rescale_layout(pos, scale=scale) + center
  1005. pos = dict(zip(node_list, pos))
  1006. if store_pos_as is not None:
  1007. nx.set_node_attributes(G, pos, store_pos_as)
  1008. return pos
  1009. def spiral_layout(
  1010. G,
  1011. scale=1,
  1012. center=None,
  1013. dim=2,
  1014. resolution=0.35,
  1015. equidistant=False,
  1016. store_pos_as=None,
  1017. ):
  1018. """Position nodes in a spiral layout.
  1019. Parameters
  1020. ----------
  1021. G : NetworkX graph or list of nodes
  1022. A position will be assigned to every node in G.
  1023. scale : number (default: 1)
  1024. Scale factor for positions.
  1025. center : array-like or None
  1026. Coordinate pair around which to center the layout.
  1027. dim : int, default=2
  1028. Dimension of layout, currently only dim=2 is supported.
  1029. Other dimension values result in a ValueError.
  1030. resolution : float, default=0.35
  1031. The compactness of the spiral layout returned.
  1032. Lower values result in more compressed spiral layouts.
  1033. equidistant : bool, default=False
  1034. If True, nodes will be positioned equidistant from each other
  1035. by decreasing angle further from center.
  1036. If False, nodes will be positioned at equal angles
  1037. from each other by increasing separation further from center.
  1038. store_pos_as : str, default None
  1039. If non-None, the position of each node will be stored on the graph as
  1040. an attribute with this string as its name, which can be accessed with
  1041. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  1042. Returns
  1043. -------
  1044. pos : dict
  1045. A dictionary of positions keyed by node
  1046. Raises
  1047. ------
  1048. ValueError
  1049. If dim != 2
  1050. Examples
  1051. --------
  1052. >>> from pprint import pprint
  1053. >>> G = nx.path_graph(4)
  1054. >>> pos = nx.spiral_layout(G)
  1055. >>> nx.draw(G, pos=pos)
  1056. >>> # suppress the returned dict and store on the graph directly
  1057. >>> _ = nx.spiral_layout(G, store_pos_as="pos")
  1058. >>> pprint(nx.get_node_attributes(G, "pos"))
  1059. {0: array([-0.64153279, -0.68555087]),
  1060. 1: array([-0.03307913, -0.46344795]),
  1061. 2: array([0.34927952, 0.14899882]),
  1062. 3: array([0.32533239, 1. ])}
  1063. Notes
  1064. -----
  1065. This algorithm currently only works in two dimensions.
  1066. """
  1067. import numpy as np
  1068. if dim != 2:
  1069. raise ValueError("can only handle 2 dimensions")
  1070. G, center = _process_params(G, center, dim)
  1071. if len(G) == 0:
  1072. return {}
  1073. if len(G) == 1:
  1074. pos = {nx.utils.arbitrary_element(G): center}
  1075. if store_pos_as is not None:
  1076. nx.set_node_attributes(G, pos, store_pos_as)
  1077. return pos
  1078. pos = []
  1079. if equidistant:
  1080. chord = 1
  1081. step = 0.5
  1082. theta = resolution
  1083. theta += chord / (step * theta)
  1084. for _ in range(len(G)):
  1085. r = step * theta
  1086. theta += chord / r
  1087. pos.append([np.cos(theta) * r, np.sin(theta) * r])
  1088. else:
  1089. dist = np.arange(len(G), dtype=float)
  1090. angle = resolution * dist
  1091. pos = np.transpose(dist * np.array([np.cos(angle), np.sin(angle)]))
  1092. pos = rescale_layout(np.array(pos), scale=scale) + center
  1093. pos = dict(zip(G, pos))
  1094. if store_pos_as is not None:
  1095. nx.set_node_attributes(G, pos, store_pos_as)
  1096. return pos
  1097. def multipartite_layout(
  1098. G, subset_key="subset", align="vertical", scale=1, center=None, store_pos_as=None
  1099. ):
  1100. """Position nodes in layers of straight lines.
  1101. Parameters
  1102. ----------
  1103. G : NetworkX graph or list of nodes
  1104. A position will be assigned to every node in G.
  1105. subset_key : string or dict (default='subset')
  1106. If a string, the key of node data in G that holds the node subset.
  1107. If a dict, keyed by layer number to the nodes in that layer/subset.
  1108. align : string (default='vertical')
  1109. The alignment of nodes. Vertical or horizontal.
  1110. scale : number (default: 1)
  1111. Scale factor for positions.
  1112. center : array-like or None
  1113. Coordinate pair around which to center the layout.
  1114. store_pos_as : str, default None
  1115. If non-None, the position of each node will be stored on the graph as
  1116. an attribute with this string as its name, which can be accessed with
  1117. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  1118. Returns
  1119. -------
  1120. pos : dict
  1121. A dictionary of positions keyed by node.
  1122. Examples
  1123. --------
  1124. >>> G = nx.complete_multipartite_graph(28, 16, 10)
  1125. >>> pos = nx.multipartite_layout(G)
  1126. >>> # suppress the returned dict and store on the graph directly
  1127. >>> G = nx.complete_multipartite_graph(28, 16, 10)
  1128. >>> _ = nx.multipartite_layout(G, store_pos_as="pos")
  1129. or use a dict to provide the layers of the layout
  1130. >>> G = nx.Graph([(0, 1), (1, 2), (1, 3), (3, 4)])
  1131. >>> layers = {"a": [0], "b": [1], "c": [2, 3], "d": [4]}
  1132. >>> pos = nx.multipartite_layout(G, subset_key=layers)
  1133. Notes
  1134. -----
  1135. This algorithm currently only works in two dimensions and does not
  1136. try to minimize edge crossings.
  1137. Network does not need to be a complete multipartite graph. As long as nodes
  1138. have subset_key data, they will be placed in the corresponding layers.
  1139. """
  1140. import numpy as np
  1141. if align not in ("vertical", "horizontal"):
  1142. msg = "align must be either vertical or horizontal."
  1143. raise ValueError(msg)
  1144. G, center = _process_params(G, center=center, dim=2)
  1145. if len(G) == 0:
  1146. return {}
  1147. try:
  1148. # check if subset_key is dict-like
  1149. if len(G) != sum(len(nodes) for nodes in subset_key.values()):
  1150. raise nx.NetworkXError(
  1151. "all nodes must be in one subset of `subset_key` dict"
  1152. )
  1153. except AttributeError:
  1154. # subset_key is not a dict, hence a string
  1155. node_to_subset = nx.get_node_attributes(G, subset_key)
  1156. if len(node_to_subset) != len(G):
  1157. raise nx.NetworkXError(
  1158. f"all nodes need a subset_key attribute: {subset_key}"
  1159. )
  1160. subset_key = nx.utils.groups(node_to_subset)
  1161. # Sort by layer, if possible
  1162. try:
  1163. layers = dict(sorted(subset_key.items()))
  1164. except TypeError:
  1165. layers = subset_key
  1166. pos = None
  1167. nodes = []
  1168. width = len(layers)
  1169. for i, layer in enumerate(layers.values()):
  1170. height = len(layer)
  1171. xs = np.repeat(i, height)
  1172. ys = np.arange(0, height, dtype=float)
  1173. offset = ((width - 1) / 2, (height - 1) / 2)
  1174. layer_pos = np.column_stack([xs, ys]) - offset
  1175. if pos is None:
  1176. pos = layer_pos
  1177. else:
  1178. pos = np.concatenate([pos, layer_pos])
  1179. nodes.extend(layer)
  1180. pos = rescale_layout(pos, scale=scale) + center
  1181. if align == "horizontal":
  1182. pos = pos[:, ::-1] # swap x and y coords
  1183. pos = dict(zip(nodes, pos))
  1184. if store_pos_as is not None:
  1185. nx.set_node_attributes(G, pos, store_pos_as)
  1186. return pos
  1187. @np_random_state("seed")
  1188. def arf_layout(
  1189. G,
  1190. pos=None,
  1191. scaling=1,
  1192. a=1.1,
  1193. etol=1e-6,
  1194. dt=1e-3,
  1195. max_iter=1000,
  1196. *,
  1197. seed=None,
  1198. store_pos_as=None,
  1199. ):
  1200. """Arf layout for networkx
  1201. The attractive and repulsive forces (arf) layout [1] improves the spring
  1202. layout in three ways. First, it prevents congestion of highly connected nodes
  1203. due to strong forcing between nodes. Second, it utilizes the layout space
  1204. more effectively by preventing large gaps that spring layout tends to create.
  1205. Lastly, the arf layout represents symmetries in the layout better than the
  1206. default spring layout.
  1207. Parameters
  1208. ----------
  1209. G : nx.Graph or nx.DiGraph
  1210. Networkx graph.
  1211. pos : dict
  1212. Initial position of the nodes. If set to None a
  1213. random layout will be used.
  1214. scaling : float
  1215. Scales the radius of the circular layout space.
  1216. a : float
  1217. Strength of springs between connected nodes. Should be larger than 1.
  1218. The greater a, the clearer the separation of unconnected sub clusters.
  1219. etol : float
  1220. Gradient sum of spring forces must be larger than `etol` before successful
  1221. termination.
  1222. dt : float
  1223. Time step for force differential equation simulations.
  1224. max_iter : int
  1225. Max iterations before termination of the algorithm.
  1226. seed : int, RandomState instance or None optional (default=None)
  1227. Set the random state for deterministic node layouts.
  1228. If int, `seed` is the seed used by the random number generator,
  1229. if numpy.random.RandomState instance, `seed` is the random
  1230. number generator,
  1231. if None, the random number generator is the RandomState instance used
  1232. by numpy.random.
  1233. store_pos_as : str, default None
  1234. If non-None, the position of each node will be stored on the graph as
  1235. an attribute with this string as its name, which can be accessed with
  1236. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  1237. Returns
  1238. -------
  1239. pos : dict
  1240. A dictionary of positions keyed by node.
  1241. Examples
  1242. --------
  1243. >>> G = nx.grid_graph((5, 5))
  1244. >>> pos = nx.arf_layout(G)
  1245. >>> # suppress the returned dict and store on the graph directly
  1246. >>> G = nx.grid_graph((5, 5))
  1247. >>> _ = nx.arf_layout(G, store_pos_as="pos")
  1248. References
  1249. ----------
  1250. .. [1] "Self-Organization Applied to Dynamic Network Layout", M. Geipel,
  1251. International Journal of Modern Physics C, 2007, Vol 18, No 10,
  1252. pp. 1537-1549.
  1253. https://doi.org/10.1142/S0129183107011558 https://arxiv.org/abs/0704.1748
  1254. """
  1255. import warnings
  1256. import numpy as np
  1257. if a <= 1:
  1258. msg = "The parameter a should be larger than 1"
  1259. raise ValueError(msg)
  1260. pos_tmp = nx.random_layout(G, seed=seed)
  1261. if pos is None:
  1262. pos = pos_tmp
  1263. else:
  1264. for node in G.nodes():
  1265. if node not in pos:
  1266. pos[node] = pos_tmp[node].copy()
  1267. # Initialize spring constant matrix
  1268. N = len(G)
  1269. # No nodes no computation
  1270. if N == 0:
  1271. return pos
  1272. # init force of springs
  1273. K = np.ones((N, N)) - np.eye(N)
  1274. node_order = {node: i for i, node in enumerate(G)}
  1275. for x, y in G.edges():
  1276. if x != y:
  1277. idx, jdx = (node_order[i] for i in (x, y))
  1278. K[idx, jdx] = a
  1279. # vectorize values
  1280. p = np.asarray(list(pos.values()))
  1281. # equation 10 in [1]
  1282. rho = scaling * np.sqrt(N)
  1283. # looping variables
  1284. error = etol + 1
  1285. n_iter = 0
  1286. while error > etol:
  1287. diff = p[:, np.newaxis] - p[np.newaxis]
  1288. A = np.linalg.norm(diff, axis=-1)[..., np.newaxis]
  1289. # attraction_force - repulsions force
  1290. # suppress nans due to division; caused by diagonal set to zero.
  1291. # Does not affect the computation due to nansum
  1292. with warnings.catch_warnings():
  1293. warnings.simplefilter("ignore")
  1294. change = K[..., np.newaxis] * diff - rho / A * diff
  1295. change = np.nansum(change, axis=0)
  1296. p += change * dt
  1297. error = np.linalg.norm(change, axis=-1).sum()
  1298. if n_iter > max_iter:
  1299. break
  1300. n_iter += 1
  1301. pos = dict(zip(G.nodes(), p))
  1302. if store_pos_as is not None:
  1303. nx.set_node_attributes(G, pos, store_pos_as)
  1304. return pos
  1305. @np_random_state("seed")
  1306. @nx._dispatchable(edge_attrs="weight", mutates_input={"store_pos_as": 15})
  1307. def forceatlas2_layout(
  1308. G,
  1309. pos=None,
  1310. *,
  1311. max_iter=100,
  1312. jitter_tolerance=1.0,
  1313. scaling_ratio=2.0,
  1314. gravity=1.0,
  1315. distributed_action=False,
  1316. strong_gravity=False,
  1317. node_mass=None,
  1318. node_size=None,
  1319. weight=None,
  1320. linlog=False,
  1321. seed=None,
  1322. dim=2,
  1323. store_pos_as=None,
  1324. ):
  1325. """Position nodes using the ForceAtlas2 force-directed layout algorithm.
  1326. This function applies the ForceAtlas2 layout algorithm [1]_ to a NetworkX graph,
  1327. positioning the nodes in a way that visually represents the structure of the graph.
  1328. The algorithm uses physical simulation to minimize the energy of the system,
  1329. resulting in a more readable layout.
  1330. Parameters
  1331. ----------
  1332. G : nx.Graph
  1333. A NetworkX graph to be laid out.
  1334. pos : dict or None, optional
  1335. Initial positions of the nodes. If None, random initial positions are used.
  1336. max_iter : int (default: 100)
  1337. Number of iterations for the layout optimization.
  1338. jitter_tolerance : float (default: 1.0)
  1339. Controls the tolerance for adjusting the speed of layout generation.
  1340. scaling_ratio : float (default: 2.0)
  1341. Determines the scaling of attraction and repulsion forces.
  1342. gravity : float (default: 1.0)
  1343. Determines the amount of attraction on nodes to the center. Prevents islands
  1344. (i.e. weakly connected or disconnected parts of the graph)
  1345. from drifting away.
  1346. distributed_action : bool (default: False)
  1347. Distributes the attraction force evenly among nodes.
  1348. strong_gravity : bool (default: False)
  1349. Applies a strong gravitational pull towards the center.
  1350. node_mass : dict or None, optional
  1351. Maps nodes to their masses, influencing the attraction to other nodes.
  1352. node_size : dict or None, optional
  1353. Maps nodes to their sizes, preventing crowding by creating a halo effect.
  1354. weight : string or None, optional (default: None)
  1355. The edge attribute that holds the numerical value used for
  1356. the edge weight. If None, then all edge weights are 1.
  1357. linlog : bool (default: False)
  1358. Uses logarithmic attraction instead of linear.
  1359. seed : int, RandomState instance or None optional (default=None)
  1360. Used only for the initial positions in the algorithm.
  1361. Set the random state for deterministic node layouts.
  1362. If int, `seed` is the seed used by the random number generator,
  1363. if numpy.random.RandomState instance, `seed` is the random
  1364. number generator,
  1365. if None, the random number generator is the RandomState instance used
  1366. by numpy.random.
  1367. dim : int (default: 2)
  1368. Sets the dimensions for the layout. Ignored if `pos` is provided.
  1369. store_pos_as : str, default None
  1370. If non-None, the position of each node will be stored on the graph as
  1371. an attribute with this string as its name, which can be accessed with
  1372. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  1373. Examples
  1374. --------
  1375. >>> import networkx as nx
  1376. >>> G = nx.florentine_families_graph()
  1377. >>> pos = nx.forceatlas2_layout(G)
  1378. >>> nx.draw(G, pos=pos)
  1379. >>> # suppress the returned dict and store on the graph directly
  1380. >>> pos = nx.forceatlas2_layout(G, store_pos_as="pos")
  1381. >>> _ = nx.forceatlas2_layout(G, store_pos_as="pos")
  1382. References
  1383. ----------
  1384. .. [1] Jacomy, M., Venturini, T., Heymann, S., & Bastian, M. (2014).
  1385. ForceAtlas2, a continuous graph layout algorithm for handy network
  1386. visualization designed for the Gephi software. PloS one, 9(6), e98679.
  1387. https://doi.org/10.1371/journal.pone.0098679
  1388. """
  1389. import numpy as np
  1390. if len(G) == 0:
  1391. return {}
  1392. # parse optional pos positions
  1393. if pos is None:
  1394. pos = nx.random_layout(G, dim=dim, seed=seed)
  1395. pos_arr = np.array(list(pos.values()))
  1396. elif len(pos) == len(G):
  1397. pos_arr = np.array([pos[node].copy() for node in G])
  1398. else:
  1399. # set random node pos within the initial pos values
  1400. pos_init = np.array(list(pos.values()))
  1401. max_pos = pos_init.max(axis=0)
  1402. min_pos = pos_init.min(axis=0)
  1403. dim = max_pos.size
  1404. pos_arr = min_pos + seed.rand(len(G), dim) * (max_pos - min_pos)
  1405. for idx, node in enumerate(G):
  1406. if node in pos:
  1407. pos_arr[idx] = pos[node].copy()
  1408. mass = np.zeros(len(G))
  1409. size = np.zeros(len(G))
  1410. # Only adjust for size when the users specifies size other than default (1)
  1411. adjust_sizes = False
  1412. if node_size is None:
  1413. node_size = {}
  1414. else:
  1415. adjust_sizes = True
  1416. if node_mass is None:
  1417. node_mass = {}
  1418. for idx, node in enumerate(G):
  1419. mass[idx] = node_mass.get(node, G.degree(node) + 1)
  1420. size[idx] = node_size.get(node, 1)
  1421. n = len(G)
  1422. gravities = np.zeros((n, dim))
  1423. attraction = np.zeros((n, dim))
  1424. repulsion = np.zeros((n, dim))
  1425. A = nx.to_numpy_array(G, weight=weight)
  1426. def estimate_factor(n, swing, traction, speed, speed_efficiency, jitter_tolerance):
  1427. """Computes the scaling factor for the force in the ForceAtlas2 layout algorithm.
  1428. This helper function adjusts the speed and
  1429. efficiency of the layout generation based on the
  1430. current state of the system, such as the number of
  1431. nodes, current swing, and traction forces.
  1432. Parameters
  1433. ----------
  1434. n : int
  1435. Number of nodes in the graph.
  1436. swing : float
  1437. The current swing, representing the oscillation of the nodes.
  1438. traction : float
  1439. The current traction force, representing the attraction between nodes.
  1440. speed : float
  1441. The current speed of the layout generation.
  1442. speed_efficiency : float
  1443. The efficiency of the current speed, influencing how fast the layout converges.
  1444. jitter_tolerance : float
  1445. The tolerance for jitter, affecting how much speed adjustment is allowed.
  1446. Returns
  1447. -------
  1448. tuple
  1449. A tuple containing the updated speed and speed efficiency.
  1450. Notes
  1451. -----
  1452. This function is a part of the ForceAtlas2 layout algorithm and is used to dynamically adjust the
  1453. layout parameters to achieve an optimal and stable visualization.
  1454. """
  1455. import numpy as np
  1456. # estimate jitter
  1457. opt_jitter = 0.05 * np.sqrt(n)
  1458. min_jitter = np.sqrt(opt_jitter)
  1459. max_jitter = 10
  1460. min_speed_efficiency = 0.05
  1461. other = min(max_jitter, opt_jitter * traction / n**2)
  1462. jitter = jitter_tolerance * max(min_jitter, other)
  1463. if swing / traction > 2.0:
  1464. if speed_efficiency > min_speed_efficiency:
  1465. speed_efficiency *= 0.5
  1466. jitter = max(jitter, jitter_tolerance)
  1467. if swing == 0:
  1468. target_speed = np.inf
  1469. else:
  1470. target_speed = jitter * speed_efficiency * traction / swing
  1471. if swing > jitter * traction:
  1472. if speed_efficiency > min_speed_efficiency:
  1473. speed_efficiency *= 0.7
  1474. elif speed < 1000:
  1475. speed_efficiency *= 1.3
  1476. max_rise = 0.5
  1477. speed = speed + min(target_speed - speed, max_rise * speed)
  1478. return speed, speed_efficiency
  1479. speed = 1
  1480. speed_efficiency = 1
  1481. swing = 1
  1482. traction = 1
  1483. for _ in range(max_iter):
  1484. # compute pairwise difference
  1485. diff = pos_arr[:, None] - pos_arr[None]
  1486. # compute pairwise distance
  1487. distance = np.linalg.norm(diff, axis=-1)
  1488. # linear attraction
  1489. if linlog:
  1490. attraction = -np.log(1 + distance) / distance
  1491. np.fill_diagonal(attraction, 0)
  1492. attraction = np.einsum("ij, ij -> ij", attraction, A)
  1493. attraction = np.einsum("ijk, ij -> ik", diff, attraction)
  1494. else:
  1495. attraction = -np.einsum("ijk, ij -> ik", diff, A)
  1496. if distributed_action:
  1497. attraction /= mass[:, None]
  1498. # repulsion
  1499. tmp = mass[:, None] @ mass[None]
  1500. if adjust_sizes:
  1501. distance += -size[:, None] - size[None]
  1502. d2 = distance**2
  1503. # remove self-interaction
  1504. np.fill_diagonal(tmp, 0)
  1505. np.fill_diagonal(d2, 1)
  1506. factor = (tmp / d2) * scaling_ratio
  1507. repulsion = np.einsum("ijk, ij -> ik", diff, factor)
  1508. # gravity
  1509. pos_centered = pos_arr - np.mean(pos_arr, axis=0)
  1510. if strong_gravity:
  1511. gravities = -gravity * mass[:, None] * pos_centered
  1512. else:
  1513. # hide warnings for divide by zero. Then change nan to 0
  1514. with np.errstate(divide="ignore", invalid="ignore"):
  1515. unit_vec = pos_centered / np.linalg.norm(pos_centered, axis=-1)[:, None]
  1516. unit_vec = np.nan_to_num(unit_vec, nan=0)
  1517. gravities = -gravity * mass[:, None] * unit_vec
  1518. # total forces
  1519. update = attraction + repulsion + gravities
  1520. # compute total swing and traction
  1521. swing += (mass * np.linalg.norm(pos_arr - update, axis=-1)).sum()
  1522. traction += (0.5 * mass * np.linalg.norm(pos_arr + update, axis=-1)).sum()
  1523. speed, speed_efficiency = estimate_factor(
  1524. n,
  1525. swing,
  1526. traction,
  1527. speed,
  1528. speed_efficiency,
  1529. jitter_tolerance,
  1530. )
  1531. # update pos
  1532. if adjust_sizes:
  1533. df = np.linalg.norm(update, axis=-1)
  1534. swinging = mass * df
  1535. factor = 0.1 * speed / (1 + np.sqrt(speed * swinging))
  1536. factor = np.minimum(factor * df, 10.0 * np.ones(df.shape)) / df
  1537. else:
  1538. swinging = mass * np.linalg.norm(update, axis=-1)
  1539. factor = speed / (1 + np.sqrt(speed * swinging))
  1540. factored_update = update * factor[:, None]
  1541. pos_arr += factored_update
  1542. if abs(factored_update).sum() < 1e-10:
  1543. break
  1544. pos = dict(zip(G, pos_arr))
  1545. if store_pos_as is not None:
  1546. nx.set_node_attributes(G, pos, store_pos_as)
  1547. return pos
  1548. def rescale_layout(pos, scale=1):
  1549. """Returns scaled position array to (-scale, scale) in all axes.
  1550. The function acts on NumPy arrays which hold position information.
  1551. Each position is one row of the array. The dimension of the space
  1552. equals the number of columns. Each coordinate in one column.
  1553. To rescale, the mean (center) is subtracted from each axis separately.
  1554. Then all values are scaled so that the largest magnitude value
  1555. from all axes equals `scale` (thus, the aspect ratio is preserved).
  1556. The resulting NumPy Array is returned (order of rows unchanged).
  1557. Parameters
  1558. ----------
  1559. pos : numpy array
  1560. positions to be scaled. Each row is a position.
  1561. scale : number (default: 1)
  1562. The size of the resulting extent in all directions.
  1563. attribute : str, default None
  1564. If non-None, the position of each node will be stored on the graph as
  1565. an attribute named `attribute` which can be accessed with
  1566. `G.nodes[...][attribute]`. The function still returns the dictionary.
  1567. Returns
  1568. -------
  1569. pos : numpy array
  1570. scaled positions. Each row is a position.
  1571. See Also
  1572. --------
  1573. rescale_layout_dict
  1574. """
  1575. import numpy as np
  1576. # Find max length over all dimensions
  1577. pos -= pos.mean(axis=0)
  1578. lim = np.abs(pos).max() # max coordinate for all axes
  1579. # rescale to (-scale, scale) in all directions, preserves aspect
  1580. if lim > 0:
  1581. pos *= scale / lim
  1582. return pos
  1583. def rescale_layout_dict(pos, scale=1):
  1584. """Return a dictionary of scaled positions keyed by node
  1585. Parameters
  1586. ----------
  1587. pos : A dictionary of positions keyed by node
  1588. scale : number (default: 1)
  1589. The size of the resulting extent in all directions.
  1590. Returns
  1591. -------
  1592. pos : A dictionary of positions keyed by node
  1593. Examples
  1594. --------
  1595. >>> import numpy as np
  1596. >>> pos = {0: np.array((0, 0)), 1: np.array((1, 1)), 2: np.array((0.5, 0.5))}
  1597. >>> nx.rescale_layout_dict(pos)
  1598. {0: array([-1., -1.]), 1: array([1., 1.]), 2: array([0., 0.])}
  1599. >>> pos = {0: np.array((0, 0)), 1: np.array((-1, 1)), 2: np.array((-0.5, 0.5))}
  1600. >>> nx.rescale_layout_dict(pos, scale=2)
  1601. {0: array([ 2., -2.]), 1: array([-2., 2.]), 2: array([0., 0.])}
  1602. See Also
  1603. --------
  1604. rescale_layout
  1605. """
  1606. import numpy as np
  1607. if not pos: # empty_graph
  1608. return {}
  1609. pos_v = np.array(list(pos.values()))
  1610. pos_v = rescale_layout(pos_v, scale=scale)
  1611. return dict(zip(pos, pos_v))
  1612. def bfs_layout(G, start, *, align="vertical", scale=1, center=None, store_pos_as=None):
  1613. """Position nodes according to breadth-first search algorithm.
  1614. Parameters
  1615. ----------
  1616. G : NetworkX graph
  1617. A position will be assigned to every node in G.
  1618. start : node in `G`
  1619. Starting node for bfs
  1620. align : string (default='vertical')
  1621. The alignment of nodes within a layer, either `"vertical"` or
  1622. `"horizontal"`.
  1623. scale : number (default: 1)
  1624. Scale factor for positions.
  1625. center : array-like or None
  1626. Coordinate pair around which to center the layout.
  1627. store_pos_as : str, default None
  1628. If non-None, the position of each node will be stored on the graph as
  1629. an attribute with this string as its name, which can be accessed with
  1630. ``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
  1631. Returns
  1632. -------
  1633. pos : dict
  1634. A dictionary of positions keyed by node.
  1635. Examples
  1636. --------
  1637. >>> from pprint import pprint
  1638. >>> G = nx.path_graph(4)
  1639. >>> pos = nx.bfs_layout(G, 0)
  1640. >>> # suppress the returned dict and store on the graph directly
  1641. >>> _ = nx.bfs_layout(G, 0, store_pos_as="pos")
  1642. >>> pprint(nx.get_node_attributes(G, "pos"))
  1643. {0: array([-1., 0.]),
  1644. 1: array([-0.33333333, 0. ]),
  1645. 2: array([0.33333333, 0. ]),
  1646. 3: array([1., 0.])}
  1647. Notes
  1648. -----
  1649. This algorithm currently only works in two dimensions and does not
  1650. try to minimize edge crossings.
  1651. """
  1652. G, center = _process_params(G, center, 2)
  1653. # Compute layers with BFS
  1654. layers = dict(enumerate(nx.bfs_layers(G, start)))
  1655. if len(G) != sum(len(nodes) for nodes in layers.values()):
  1656. raise nx.NetworkXError(
  1657. "bfs_layout didn't include all nodes. Perhaps use input graph:\n"
  1658. " G.subgraph(nx.node_connected_component(G, start))"
  1659. )
  1660. # Compute node positions with multipartite_layout
  1661. pos = multipartite_layout(
  1662. G, subset_key=layers, align=align, scale=scale, center=center
  1663. )
  1664. if store_pos_as is not None:
  1665. nx.set_node_attributes(G, pos, store_pos_as)
  1666. return pos