I am looking for a way to automatically define neighbourhoods in cities as polygons on a graph.
My definition of a neighbourhood has two parts:
A block: An area inclosed between a number of streets, where the number of streets (edges) and intersections (nodes) is a minimum of three (a triangle).
A neighbourhood: For any given block, all the blocks directly adjacent to that block and the block itself.
See this illustration for an example:
E.g. B4 is block defined by 7 nodes and 6 edges connecting them. As most of the examples here, the other blocks are defined by 4 nodes and 4 edges connecting them. Also, the neighbourhood of B1 includes B2 (and vice versa) while B2 also includes B3.
I am using osmnx to get street data from OSM.
Using osmnx and networkx, how can I traverse a graph to find the nodes and edges that define each block?
For each block, how can I find the adjacent blocks?
I am working myself towards a piece of code that takes a graph and a pair of coordinates (latitude, longitude) as input, identifies the relevant block and returns the polygon for that block and the neighbourhood as defined above.
Here is the code used to make the map:
import osmnx as ox
import networkx as nx
import matplotlib.pyplot as plt
G = ox.graph_from_address('Nørrebrogade 20, Copenhagen Municipality',
network_type='all',
distance=500)
and my attempt at finding cliques with different number of nodes and degrees.
def plot_cliques(graph, number_of_nodes, degree):
ug = ox.save_load.get_undirected(graph)
cliques = nx.find_cliques(ug)
cliques_nodes = [clq for clq in cliques if len(clq) >= number_of_nodes]
print("{} cliques with more than {} nodes.".format(len(cliques_nodes), number_of_nodes))
nodes = set(n for clq in cliques_nodes for n in clq)
h = ug.subgraph(nodes)
deg = nx.degree(h)
nodes_degree = [n for n in nodes if deg[n] >= degree]
k = h.subgraph(nodes_degree)
nx.draw(k, node_size=5)
Theory that might be relevant:
Enumerating All Cycles in an Undirected Graph
Finding city blocks using the graph is surprisingly non-trivial.
Basically, this amounts to finding the smallest set of smallest rings (SSSR), which is an NP-complete problem.
A review of this problem (and related problems) can be found here.
On SO, there is one description of an algorithm to solve it here.
As far as I can tell, there is no corresponding implementation in networkx (or in python for that matter).
I tried this approach briefly and then abandoned it -- my brain is not up to scratch for that kind of work today.
That being said, I will award a bounty to anybody that might visit this page at a later date and post a tested implementation of an algorithm that finds the SSSR in python.
I have instead pursued a different approach, leveraging the fact that the graph is guaranteed to be planar.
Briefly, instead of treating this as a graph problem, we treat this as an image segmentation problem.
First, we find all connected regions in the image. We then determine the contour around each region,
transform the contours in image coordinates back to longitudes and latitudes.
Given the following imports and function definitions:
#!/usr/bin/env python
# coding: utf-8
"""
Find house blocks in osmnx graphs.
"""
import numpy as np
import osmnx as ox
import networkx as nx
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import PathPatch
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from skimage.measure import label, find_contours, points_in_poly
from skimage.color import label2rgb
ox.config(log_console=True, use_cache=True)
def k_core(G, k):
H = nx.Graph(G, as_view=True)
H.remove_edges_from(nx.selfloop_edges(H))
core_nodes = nx.k_core(H, k)
H = H.subgraph(core_nodes)
return G.subgraph(core_nodes)
def plot2img(fig):
# remove margins
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
# convert to image
# https://stackoverflow.com/a/35362787/2912349
# https://stackoverflow.com/a/54334430/2912349
canvas = FigureCanvas(fig)
canvas.draw()
img_as_string, (width, height) = canvas.print_to_buffer()
as_rgba = np.fromstring(img_as_string, dtype='uint8').reshape((height, width, 4))
return as_rgba[:,:,:3]
Load the data. Do cache the imports, if testing this repeatedly -- otherwise your account can get banned.
Speaking from experience here.
G = ox.graph_from_address('Nørrebrogade 20, Copenhagen Municipality',
network_type='all', distance=500)
G_projected = ox.project_graph(G)
ox.save_graphml(G_projected, filename='network.graphml')
# G = ox.load_graphml('network.graphml')
Prune nodes and edges that cannot be part of a cycle.
This step is not strictly necessary but results in nicer contours.
H = k_core(G, 2)
fig1, ax1 = ox.plot_graph(H, node_size=0, edge_color='k', edge_linewidth=1)
Convert plot to image and find connected regions:
img = plot2img(fig1)
label_image = label(img > 128)
image_label_overlay = label2rgb(label_image[:,:,0], image=img[:,:,0])
fig, ax = plt.subplots(1,1)
ax.imshow(image_label_overlay)
For each labelled region, find the contour and convert the contour pixel coordinates back to data coordinates.
# using a large region here as an example;
# however we could also loop over all unique labels, i.e.
# for ii in np.unique(labels.ravel()):
ii = np.argsort(np.bincount(label_image.ravel()))[-5]
mask = (label_image[:,:,0] == ii)
contours = find_contours(mask.astype(np.float), 0.5)
# Select the largest contiguous contour
contour = sorted(contours, key=lambda x: len(x))[-1]
# display the image and plot the contour;
# this allows us to transform the contour coordinates back to the original data cordinates
fig2, ax2 = plt.subplots()
ax2.imshow(mask, interpolation='nearest', cmap='gray')
ax2.autoscale(enable=False)
ax2.step(contour.T[1], contour.T[0], linewidth=2, c='r')
plt.close(fig2)
# first column indexes rows in images, second column indexes columns;
# therefor we need to swap contour array to get xy values
contour = np.fliplr(contour)
pixel_to_data = ax2.transData + ax2.transAxes.inverted() + ax1.transAxes + ax1.transData.inverted()
transformed_contour = pixel_to_data.transform(contour)
transformed_contour_path = Path(transformed_contour, closed=True)
patch = PathPatch(transformed_contour_path, facecolor='red')
ax1.add_patch(patch)
Determine all points in the original graph that fall inside (or on) the contour.
x = G.nodes.data('x')
y = G.nodes.data('y')
xy = np.array([(x[node], y[node]) for node in G.nodes])
eps = (xy.max(axis=0) - xy.min(axis=0)).mean() / 100
is_inside = transformed_contour_path.contains_points(xy, radius=-eps)
nodes_inside_block = [node for node, flag in zip(G.nodes, is_inside) if flag]
node_size = [50 if node in nodes_inside_block else 0 for node in G.nodes]
node_color = ['r' if node in nodes_inside_block else 'k' for node in G.nodes]
fig3, ax3 = ox.plot_graph(G, node_color=node_color, node_size=node_size)
Figuring out if two blocks are neighbors is pretty easy. Just check if they share a node:
if set(nodes_inside_block_1) & set(nodes_inside_block_2): # empty set evaluates to False
print("Blocks are neighbors.")
I'm not completely sure that cycle_basis will give you the neighborhoods you seek, but if it does, it's a simple thing to get the neighborhood graph from it:
import osmnx as ox
import networkx as nx
import matplotlib.pyplot as plt
G = ox.graph_from_address('Nørrebrogade 20, Copenhagen Municipality',
network_type='all',
distance=500)
H = nx.Graph(G) # make a simple undirected graph from G
cycles = nx.cycles.cycle_basis(H) # I think a cycle basis should get all the neighborhoods, except
# we'll need to filter the cycles that are too small.
cycles = [set(cycle) for cycle in cycles if len(cycle) > 2] # Turn the lists into sets for next loop.
# We can create a new graph where the nodes are neighborhoods and two neighborhoods are connected if
# they are adjacent:
I = nx.Graph()
for i, n in enumerate(cycles):
for j, m in enumerate(cycles[i + 1:], start=i + 1):
if not n.isdisjoint(m):
I.add_edge(i, j)
I appreciate this question is a little bit old now but I have an alternative approach that is relatively straighforward - it does require stepping away from networkx for a moment though.
Creating the blocks
Get the projected graph:
import osmnx as ox
import geopandas as gpd
from shapely.ops import polygonize
G = ox.graph_from_address('Nørrebrogade 20, Copenhagen Municipality',
network_type='all',
dist=500)
G_projected = ox.project_graph(G)
Convert the graph to undirected - this removes duplicate edges that would cause the subsequent polygonization to fail:
G_undirected = G_projected.to_undirected()
Extract just the edges into a GeoPandas GeoDataFrame:
G_edges_as_gdf = ox.graph_to_gdfs(G_undirected, nodes=False, edges=True)
Use polygonize from shapely.ops on the edges to create the block faces and then use these as the geometry in a new GeoDataFrame:
block_faces = list(polygonize(G_edges_as_gdf['geometry']))
blocks = gpd.GeoDataFrame(geometry=block_faces)
Plot the result:
ax = G_edges_as_gdf.plot(figsize=(10,10), color='red', zorder=0)
blocks.plot(ax=ax, facecolor='gainsboro', edgecolor='k', linewidth=2, alpha=0.5, zorder=1)
blocks created from line fragments by shapely.ops.polygonize()
Finding neighbors
PySAL does a great job of this with its spatial weights see https://pysal.org/notebooks/lib/libpysal/weights.html for further information. libpysal can be installed from conda-forge.
Here we use Rook weights to identify blocks that share an edge as in the original question. Queen weights would also include those that only share a node (i.e. meet at a street junction)
from libpysal.weights import Rook # Queen, KNN also available
w_rook = Rook.from_dataframe(blocks)
The spatial weights are just for the neighbours so we need to append the original block (index number 18 here just as an example):
block = 18
neighbors = w_rook.neighbors[block]
neighbors.append(block)
neighbors
Then we can plot using neighbors as a filter:
ax = blocks.plot(figsize=(10,10), facecolor='gainsboro', edgecolor='black')
blocks[blocks.index.isin(neighbors)].plot(ax=ax, color='red', alpha=0.5)
neighboring blocks highlighted on top of all blocks
Notes
This doesn't resolve the sliver polygons caused by multiple road centre lines and it can be challenging to resolve ambiguities caused by pedestrianised streets and footpaths - presumably a pedestrianised street is a legitimate division between blocks but a footpath may not be.
From memory I have in the past needed to fragment the LineStrings created from the edges further before polygonizing them - this didn't seem to be necessary with this example.
I have left out the final step of linking the new geometries back to the Node IDs with some kind of spatial join etc.
I don't have a code, but I guess that once i'm on the sidewalk, if I keep turning to the right at each corner, I will cycle through the edges of my block. I don't know the libraries so I'll just talk algo here.
from your point, go north until you reach a street
turn right as much as you can and walk on the street
on the next corner, find all the steets, chose the one that makes the smallest angle with your street counting from the right.
walk on that street.
turn right, etc.
It's actually an algorithm to use to exit a maze : keep your right hand on the wall and walk. It doesn't work in case of loops in the maze, you just loop around. But it gives a solution to your problem.
This is an implementation of Hashemi Emad's idea. It works well as long as the starting position is chosen such that there exists a way to step counterclockwise in a tight circle. For some edges, in particular around the outside of the map, this is not possible. I don't have an idea how to select good starting positions, or how to filter solutions -- but maybe somebody else has one.
Working example (starting with edge (1204573687, 4555480822)):
Example, where this approach does not work (starting with edge (1286684278, 5818325197)):
Code
#!/usr/bin/env python
# coding: utf-8
"""
Find house blocks in osmnx graphs.
"""
import numpy as np
import networkx as nx
import osmnx as ox
import matplotlib.pyplot as plt; plt.ion()
from matplotlib.path import Path
from matplotlib.patches import PathPatch
ox.config(log_console=True, use_cache=True)
def k_core(G, k):
H = nx.Graph(G, as_view=True)
H.remove_edges_from(nx.selfloop_edges(H))
core_nodes = nx.k_core(H, k)
H = H.subgraph(core_nodes)
return G.subgraph(core_nodes)
def get_vector(G, n1, n2):
dx = np.diff([G.nodes.data()[n]['x'] for n in (n1, n2)])
dy = np.diff([G.nodes.data()[n]['y'] for n in (n1, n2)])
return np.array([dx, dy])
def angle_between(v1, v2):
# https://stackoverflow.com/a/31735642/2912349
ang1 = np.arctan2(*v1[::-1])
ang2 = np.arctan2(*v2[::-1])
return (ang1 - ang2) % (2 * np.pi)
def step_counterclockwise(G, edge, path):
start, stop = edge
v1 = get_vector(G, stop, start)
neighbors = set(G.neighbors(stop))
candidates = list(set(neighbors) - set([start]))
if not candidates:
raise Exception("Ran into a dead end!")
else:
angles = np.zeros_like(candidates, dtype=float)
for ii, neighbor in enumerate(candidates):
v2 = get_vector(G, stop, neighbor)
angles[ii] = angle_between(v1, v2)
next_node = candidates[np.argmin(angles)]
if next_node in path:
# next_node might not be the same as the first node in path;
# therefor, we backtrack until we end back at next_node
closed_path = [next_node]
for node in path[::-1]:
closed_path.append(node)
if node == next_node:
break
return closed_path[::-1] # reverse to have counterclockwise path
else:
path.append(next_node)
return step_counterclockwise(G, (stop, next_node), path)
def get_city_block_patch(G, boundary_nodes, *args, **kwargs):
xy = []
for node in boundary_nodes:
x = G.nodes.data()[node]['x']
y = G.nodes.data()[node]['y']
xy.append((x, y))
path = Path(xy, closed=True)
return PathPatch(path, *args, **kwargs)
if __name__ == '__main__':
# --------------------------------------------------------------------------------
# load data
# # DO CACHE RESULTS -- otherwise you can get banned for repeatedly querying the same address
# G = ox.graph_from_address('Nørrebrogade 20, Copenhagen Municipality',
# network_type='all', distance=500)
# G_projected = ox.project_graph(G)
# ox.save_graphml(G_projected, filename='network.graphml')
G = ox.load_graphml('network.graphml')
# --------------------------------------------------------------------------------
# prune nodes and edges that should/can not be part of a cycle;
# this also reduces the chance of running into a dead end when stepping counterclockwise
H = k_core(G, 2)
# --------------------------------------------------------------------------------
# pick an edge and step counterclockwise until you complete a circle
# random edge
total_edges = len(H.edges)
idx = np.random.choice(total_edges)
start, stop, _ = list(H.edges)[idx]
# good edge
# start, stop = 1204573687, 4555480822
# bad edge
# start, stop = 1286684278, 5818325197
steps = step_counterclockwise(H, (start, stop), [start, stop])
# --------------------------------------------------------------------------------
# plot
patch = get_city_block_patch(G, steps, facecolor='red', edgecolor='red', zorder=-1)
node_size = [100 if node in steps else 20 for node in G.nodes]
node_color = ['crimson' if node in steps else 'black' for node in G.nodes]
fig1, ax1 = ox.plot_graph(G, node_size=node_size, node_color=node_color, edge_color='k', edge_linewidth=1)
ax1.add_patch(patch)
fig1.savefig('city_block.png')
Related
I want to visualise some points on a graph, the points move along the link, but they are not nodes. Currently I have added some point location, but can not display them on the figure.
This is the code
# -- coding: utf-8 --
import networkx as nx
import matplotlib.pyplot as plt
import itertools
import math
#from mesa.space import NetworkGrid
#from mesa import Agent, Model
#from mesa.time import RandomActivation
#from mesa.datacollection import DataCollector
#from mesa.space import NetworkGrid
#%%Build a graph
G=nx.Graph()
G.add_node("GPs")
G.add_node("AcuteCares")
G.add_node("Waitlists")
G.add_node("newPatients")
G.add_node("Preventabledeaths")
G.add_node("ReviewPatients")
G.add_node("DeathPools")
G.add_node("DNAPool1s")
G.add_node("DNAPool2s")
G.add_node("UntreatedPopulations")
G.add_node("SAPops")
labeldict = {}
labeldict["GPs"] = "GP"
labeldict["AcuteCares"] = "Acute Care"
labeldict["Waitlists"] = "Waitlist"
labeldict["newPatients"] = "New Patients"
labeldict["Preventabledeaths"] = "Preventable Deaths"
labeldict["ReviewPatients"] = "Review Patients"
labeldict["DeathPools"] = "Natural Deaths"
labeldict["DNAPool1s"] = "First DNA"
labeldict["DNAPool2s"] = "Second DNA"
labeldict["UntreatedPopulations"] = "Untreated Population"
labeldict["SAPops"] = "General Population"
G.node["Preventabledeaths"]['pos']=(0,6)
G.node["ReviewPatients"]['pos']=(-3,5)
G.node["UntreatedPopulations"]['pos']=(3,5)
G.node["DNAPool2s"]['pos']=(-3,3)
G.node["Waitlists"]['pos']=(3,3)
G.node["AcuteCares"]['pos']=(-5,0)
G.node["DNAPool1s"]['pos']=(5,0)
G.node["GPs"]['pos']=(-3,-5)
G.node["DeathPools"]['pos']=(3,-5)
G.node["SAPops"]['pos']=(-3,-3)
G.node["newPatients"]['pos']=(3,-3)
edges=itertools.permutations(G.nodes(),2)
G.add_edges_from(edges)
pos=nx.get_node_attributes(G,'pos')
nx.draw(G,pos,labels=labeldict, with_labels = True)
plt.show()
#grid = NetworkGrid(G)
# %%
def arclen(edge):
"""
calculate the length of an edge. The format of edge is like: ('UntreatedPopulations', 'SAPops')
"""
dist_edge = math.sqrt((G.node[edge[0]]['pos'][0] - G.node[edge[1]]['pos'][0])**2 + (G.node[edge[0]]['pos'][1] - G.node[edge[1]]['pos'][1])**2)
return dist_edge
def patientcor(speed,step,edge):
"""get the coordinate of point along the edge, speed is the moving speed per step,
time is the number of steps, edge is the specific edge
"""
x=G.node[edge[0]]['pos'][0] + speed*step/arclen(edge) *(G.node[edge[1]]['pos'][0] -G.node[edge[0]]['pos'][0])
y=G.node[edge[0]]['pos'][1] + speed*step/arclen(edge) *(G.node[edge[1]]['pos'][1] -G.node[edge[0]]['pos'][1])
return (x,y)
#%% Visualise the graph, set the speed at 0.2, time is 0,1,2
edge=("SAPops","GPs")
for t in range(3):
pos[t]=patientcor(0.2, t,edge) #add the location of point on the link per step to the dict
nx.draw(G,pos, labels=labeldict,with_labels = True) #visualise pos dict along with the graph, but the additional points other than nodes do not appear on the figure
plt.show()
The graph figure only displays the nodes, but not the points that move along the edges:
The nx.draw command will only plot those nodes that are in the graph. If your dictionary pos provides locations of other points, it will silently ignore them. I believe this is the appropriate behavior and I can think of lots of times where my coding would be much more difficult if it would also plot other points that appeared in my pos dictionary.
For what you want, simply create a new list of the points you want to plot (or in your example it looks like just a single point). Then use matplotlib's scatter command.
#stuff skipped here
edge=("SAPops","GPs")
for t in range(3):
mypoint = patientcor(0.2, t,edge)
nx.draw(G,pos, labels=labeldict,with_labels = True) #visualise pos dict along with the graph, but the additional points other than nodes do not appear on the figure
plt.scatter([mypoint[0]], [mypoint[1]])
plt.show()
You'll probably want to play with the node sizes and specific locations of these points.
How can I draw a graph with it's communities using python networkx like this image :
image url
The documentation for networkx.draw_networkx_nodes and networkx.draw_networkx_edges explains how to set the node and edge colors. The patches bounding the communities can be made by finding the positions of the nodes for each community and then drawing a patch (e.g. matplotlib.patches.Circle) that contains all positions (and then some).
The hard bit is the graph layout / setting the node positions.
AFAIK, there is no routine in networkx to achieve the desired graph layout "out of the box". What you want to do is the following:
Position the communities with respect to each other: create a new, weighted graph, where each node corresponds to a community, and the weights correspond to the number of edges between communities. Get a decent layout with your favourite graph layout algorithm (e.g.spring_layout).
Position the nodes within each community: for each community, create a new graph. Find a layout for the subgraph.
Combine node positions in 1) and 3). E.g. scale community positions calculated in 1) by a factor of 10; add those values to the positions of all nodes (as computed in 2)) within that community.
I have been wanting to implement this for a while. I might do it later today or over the weekend.
EDIT:
Voila. Now you just need to draw your favourite patch around (behind) the nodes.
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
def community_layout(g, partition):
"""
Compute the layout for a modular graph.
Arguments:
----------
g -- networkx.Graph or networkx.DiGraph instance
graph to plot
partition -- dict mapping int node -> int community
graph partitions
Returns:
--------
pos -- dict mapping int node -> (float x, float y)
node positions
"""
pos_communities = _position_communities(g, partition, scale=3.)
pos_nodes = _position_nodes(g, partition, scale=1.)
# combine positions
pos = dict()
for node in g.nodes():
pos[node] = pos_communities[node] + pos_nodes[node]
return pos
def _position_communities(g, partition, **kwargs):
# create a weighted graph, in which each node corresponds to a community,
# and each edge weight to the number of edges between communities
between_community_edges = _find_between_community_edges(g, partition)
communities = set(partition.values())
hypergraph = nx.DiGraph()
hypergraph.add_nodes_from(communities)
for (ci, cj), edges in between_community_edges.items():
hypergraph.add_edge(ci, cj, weight=len(edges))
# find layout for communities
pos_communities = nx.spring_layout(hypergraph, **kwargs)
# set node positions to position of community
pos = dict()
for node, community in partition.items():
pos[node] = pos_communities[community]
return pos
def _find_between_community_edges(g, partition):
edges = dict()
for (ni, nj) in g.edges():
ci = partition[ni]
cj = partition[nj]
if ci != cj:
try:
edges[(ci, cj)] += [(ni, nj)]
except KeyError:
edges[(ci, cj)] = [(ni, nj)]
return edges
def _position_nodes(g, partition, **kwargs):
"""
Positions nodes within communities.
"""
communities = dict()
for node, community in partition.items():
try:
communities[community] += [node]
except KeyError:
communities[community] = [node]
pos = dict()
for ci, nodes in communities.items():
subgraph = g.subgraph(nodes)
pos_subgraph = nx.spring_layout(subgraph, **kwargs)
pos.update(pos_subgraph)
return pos
def test():
# to install networkx 2.0 compatible version of python-louvain use:
# pip install -U git+https://github.com/taynaud/python-louvain.git#networkx2
from community import community_louvain
g = nx.karate_club_graph()
partition = community_louvain.best_partition(g)
pos = community_layout(g, partition)
nx.draw(g, pos, node_color=list(partition.values())); plt.show()
return
Addendum
Although the general idea is sound, my old implementation above has a few issues. Most importantly, the implementation doesn't work very well for unevenly sized communities. Specifically, _position_communities gives each community the same amount of real estate on the canvas. If some of the communities are much larger than others, these communities end up being compressed into the same amount of space as the small communities. Obviously, this does not reflect the structure of the graph very well.
I have written a library for visualizing networks, which is called netgraph. It includes an improved version of the community layout routine outlined above, which also considers the sizes of the communities when arranging them. It is fully compatible with networkx and igraph Graph objects, so it should be easy and fast to make great looking graphs (at least that is the idea).
import matplotlib.pyplot as plt
import networkx as nx
# installation easiest via pip:
# pip install netgraph
from netgraph import Graph
# create a modular graph
partition_sizes = [10, 20, 30, 40]
g = nx.random_partition_graph(partition_sizes, 0.5, 0.1)
# since we created the graph, we know the best partition:
node_to_community = dict()
node = 0
for community_id, size in enumerate(partition_sizes):
for _ in range(size):
node_to_community[node] = community_id
node += 1
# # alternatively, we can infer the best partition using Louvain:
# from community import community_louvain
# node_to_community = community_louvain.best_partition(g)
community_to_color = {
0 : 'tab:blue',
1 : 'tab:orange',
2 : 'tab:green',
3 : 'tab:red',
}
node_color = {node: community_to_color[community_id] for node, community_id in node_to_community.items()}
Graph(g,
node_color=node_color, node_edge_width=0, edge_alpha=0.1,
node_layout='community', node_layout_kwargs=dict(node_to_community=node_to_community),
edge_layout='bundled', edge_layout_kwargs=dict(k=2000),
)
plt.show()
I would like to know if there is a way to draw nested networkx graphs in python.
I can successfully draw these graphs using the nx.draw_(...) method call as described in the networkx docs, but the case I'm using it for requires that one of the nodes itself is a graph (Imagine a network of rooms, at the top level with a network of areas/zones within those rooms at the next level down). I would like to show this using matplotlib or similar.
Any ideas would be appreciated.
Edit
You can probably do better than my original answer by defining a recursive function. Here is a rough outline of how that recursive function would look. My answer below gives a less elegant approach that can be easily tuned for a specific case, but if you're ever doing this frequently, you'll probably want this recursive version.
def recursive_draw(G,currentscalefactor=0.1,center_loc=(0,0),nodesize=300, shrink=0.1):
pos = nx.spring_layout(G)
scale(pos,currentscalefactor) #rescale distances to be smaller
shift(pos,center_loc) #you'll have to write your own code to shift all positions to be centered at center_loc
nx.draw(G,pos=pos, nodesize=nodesize)
for node in G.nodes_iter():
if type(node)==Graph: # or diGraph etc...
recursive_draw(node,currentscalefactor=shrink*currentscalefactor,center_loc=pos[node], nodesize = nodesize*shrink, shrink=shrink)
If anyone creates the recursive function, please add it as a separate answer, and give me a comment. I'll point to it from this answer.
Original answer
Here's a first pass (I'll hopefully edit to a complete answer by end of day, but I think this will get you most of the way there):
import networkx as nx
import pylab as py
G = nx.Graph()
H = nx.Graph()
H.add_edges_from([(1,2), (2,3), (1,3)])
I = nx.Graph()
I.add_edges_from([(1,3), (3,2)])
G.add_edge(H,I)
Gpos = nx.spring_layout(G)
Hpos = nx.spring_layout(H)
Ipos = nx.spring_layout(I)
scalefactor = 0.1
for node in H.nodes():
Hpos[node] = Hpos[node]*scalefactor + Gpos[H]
for node in I.nodes():
Ipos[node] = Ipos[node]*scalefactor + Gpos[I]
nx.draw_networkx_edges(G, pos = Gpos)
nx.draw_networkx_nodes(G, pos = Gpos, node_color = 'b', node_size = 15000, alpha = 0.5)
nx.draw(H, pos = Hpos, with_labels = True)
nx.draw(I, pos = Ipos, with_labels = True)
py.savefig('tmp.png')
The main additional thing I think you should do is to center each subnode. This would require identifying xmin, xmax, ymin, and ymax for each subplot and adjusting. You may also want to play with the scalefactor.
Using Networkx in Python, I'm trying to visualise how different movie critics are biased towards certain production companies. To show this in a graph, my idea is to fix the position of each production-company-node to an individual location in a circle, and then use the spring_layout algorithm to position the remaining movie-critic-nodes, such that one can easily see how some critics are drawn more towards certain production companies.
My problem is that I can't seem to fix the initial position of the production-company-nodes. Surely, I can fix their position but then it is just random, and I don't want that - I want them in a circle. I can calculate the position of all nodes and afterwards set the position of the production-company-nodes, but this beats the purpose of using a spring_layout algorithm and I end up with something wacky like:
Any ideas on how to do this right?
Currently my code does this:
def get_coordinates_in_circle(n):
return_list = []
for i in range(n):
theta = float(i)/n*2*3.141592654
x = np.cos(theta)
y = np.sin(theta)
return_list.append((x,y))
return return_list
G_pc = nx.Graph()
G_pc.add_edges_from(edges_2212)
fixed_nodes = []
for n in G_pc.nodes():
if n in production_companies:
fixed_nodes.append(n)
pos = nx.spring_layout(G_pc,fixed=fixed_nodes)
circular_positions = get_coordinates_in_circle(len(dps_2211))
i = 0
for p in pos.keys():
if p in production_companies:
pos[p] = circular_positions[i]
i += 1
colors = get_node_colors(G_pc, "gender")
nx.draw_networkx_nodes(G_pc, pos, cmap=plt.get_cmap('jet'), node_color=colors, node_size=50, alpha=0.5)
nx.draw_networkx_edges(G_pc,pos, alpha=0.01)
plt.show()
To create a graph and set a few positions:
import networkx as nx
G=nx.Graph()
G.add_edges_from([(1,2),(2,3),(3,1),(1,4)]) #define G
fixed_positions = {1:(0,0),2:(-1,2)}#dict with two of the positions set
fixed_nodes = fixed_positions.keys()
pos = nx.spring_layout(G,pos=fixed_positions, fixed = fixed_nodes)
nx.draw_networkx(G,pos)
Your problem appears to be that you calculate the positions of all the nodes before you set the positions of the fixed nodes.
Move pos = nx.spring_layout(G_pc,fixed=fixed_nodes) to after you set pos[p] for the fixed nodes, and change it to pos = nx.spring_layout(G_pc,pos=pos,fixed=fixed_nodes)
The dict pos stores the coordinates of each node. You should have a quick look at the documentation. In particular,
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 nuse random initial positions.
fixed : list or None optional (default=None).
Nodes to keep fixed at initial position.
list or None optional (default=None)
You're telling it to keep those nodes fixed at their initial position, but you haven't told them what that initial position should be. So I would believe it takes a random guess for that initial position, and holds it fixed. However, when I test this, it looks like I run into an error. It appears that if I tell (my version of) networkx to hold nodes in [1,2] as fixed, but I don't tell it what their positions are, I get an error (at bottom of this answer). So I'm surprised your code is running.
For some other improvements to the code using list comprehensions:
def get_coordinates_in_circle(n):
thetas = [2*np.pi*(float(i)/n) for i in range(n)]
returnlist = [(np.cos(theta),np.sin(theta)) for theta in thetas]
return return_list
G_pc = nx.Graph()
G_pc.add_edges_from(edges_2212)
circular_positions = get_coordinates_in_circle(len(dps_2211))
#it's not clear to me why you don't define circular_positions after
#fixed_nodes with len(fixed_nodes) so that they are guaranteed to
#be evenly spaced.
fixed_nodes = [n for n in G_pc.nodes() if n in production_companies]
pos = {}
for i,p in enumerate(fixed_nodes):
pos[p] = circular_positions[i]
colors = get_node_colors(G_pc, "gender")
pos = nx.spring_layout(G_pc,pos=pos, fixed=fixed_nodes)
nx.draw_networkx_nodes(G_pc, pos, cmap=plt.get_cmap('jet'), node_color=colors, node_size=50, alpha=0.5)
nx.draw_networkx_edges(G_pc,pos, alpha=0.01)
plt.show()
Here's the error I see:
import networkx as nx
G=nx.Graph()
G.add_edge(1,2)
pos = nx.spring_layout(G, fixed=[1,2])
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-4-e9586af20cc2> in <module>()
----> 1 pos = nx.spring_layout(G, fixed=[1,2])
.../networkx/drawing/layout.pyc in fruchterman_reingold_layout(G, dim, k, pos, fixed, iterations, weight, scale)
253 # We must adjust k by domain size for layouts that are not near 1x1
254 nnodes,_ = A.shape
--> 255 k=dom_size/np.sqrt(nnodes)
256 pos=_fruchterman_reingold(A,dim,k,pos_arr,fixed,iterations)
257 if fixed is None:
UnboundLocalError: local variable 'dom_size' referenced before assignment
I want to create the following image patterns using Python.
For clarity: these are two separate image sequences (one on the top row, one on the bottom row).
They are related to each other as they are projected areas of stacked tetrahedra.
In a 3D environment it looks as follows:
Note that these 3D object are not scaled such that the total object dimension remains the same. This is the case with the projected areas shown above.
The four level structure (not shown) would have an additional 10 cells on top.
The total amount of cells C at level n is:
C = (n^3 + 3*n^2 + 2*n)/6
Now I'm creating the patterns by hand (make 3D object, render out projected area, repeat) but this is very tedious and not feasible for more subdivisions.
I managed to create a single polygon with the following code, but I can't figure out how to loop this such that the total edge length stays the same, but the polygon gets subdivided in the way visualised above.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
el = 1
dfv = 1/np.sqrt(3)*el
dfe = 1/(2*np.sqrt(3))*el
vertices1 = [(0,0),(0.5*el,-dfe),(0,dfv),(-0.5*el,-dfe)]
vertices2 = [(0.5*el,-dfe),(0,dfv),(-0.5*el,-dfe)]
fig = plt.figure()
ax1 = fig.add_subplot(121)
ax1.add_patch(Polygon(vertices1, closed=True, fill=True))
ax1.set_xlim([-1, 1])
ax1.set_ylim([-1, 1])
ax1.set_aspect('equal')
ax2 = fig.add_subplot(122)
ax2.add_patch(Polygon(vertices2, closed=True, fill=True))
ax2.set_xlim([-1, 1])
ax2.set_ylim([-1, 1])
ax2.set_aspect('equal')
plt.show()
I used matplotlib and the included Polygon patch, but I'm not sure if that is the most optimal method.
Also the orientation of the polygon or the color is of no importance.
Helper class:
class Custom_Polygon(object):
"""docstring for Polygon"""
def __init__(self, verts):
self.verticies = np.array(verts)
self.dims = self.verticies.shape[1]
def scale(self, scaleFactor):
scaleMatrix = np.zeros((self.dims, self.dims))
np.fill_diagonal(scaleMatrix, scaleFactor)
self.verticies = np.dot(self.verticies, scaleMatrix)
def scale_with_orgin(self, scaleFactor, origin):
origin = origin.copy()
self.translate([-i for i in origin])
self.scale(scaleFactor)
self.translate([i for i in origin])
def translate(self, shiftBy):
self.verticies += shiftBy
def get_width(self):
x_min = self.verticies[:,0].min()
x_max = self.verticies[:,0].max()
return abs(x_max - x_min)
def get_height(self):
y_min = self.verticies[:,1].min()
y_max = self.verticies[:,1].max()
return abs(y_max - y_min)
Made a helper class for scaling and translating the polygon around to make the pattern. And wrote the algorithm for drawing the first pattern. Should not be hard to make a similar algorithm for drawing the second pattern.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
def makeFirstPattern(ax, d, verticies, scale=True):
Pn = Custom_Polygon(verticies)
direction = 1
divisions = d
if scale: Pn.scale_with_orgin(2.0/(divisions+1), Pn.verticies[-1])
for i in range(divisions, 0, -2):
for _ in range(i):
ax.add_patch(Polygon(Pn.verticies, closed=True, fill=True, ec='none'))
Pn.translate([direction * Pn.get_width()/2, 0])
direction *= -1
Pn.translate([direction * Pn.get_width(), Pn.get_height()])
el = 1
dfv = 1/np.sqrt(3)*el
dfe = 1/(2*np.sqrt(3))*el
vertices1 = [(0,0),(0.5*el,-dfe),(0,dfv),(-0.5*el,-dfe)]
vertices2 = [(0.5*el,-dfe),(0,dfv),(-0.5*el,-dfe)]
fig = plt.figure()
ax1 = fig.add_subplot(111)
makeFirstPattern(ax1, 7, vertices2)
ax1.set_xlim([-1, 1])
ax1.set_ylim([-1, 1])
ax1.set_aspect('equal')
plt.show()
For first pattern you can use this code:
from __future__ import division
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
nrows = 5
pat = np.array([[0,0],[0.5,np.sqrt(0.75)],[-0.5,np.sqrt(0.75)]])
fig = plt.figure()
ax = fig.add_subplot(111)
for base in range(nrows):
npat = 2*base + 1
for col in np.linspace(-base/2, base/2, npat):
pp = Polygon(pat + np.array([col, base*v]), fc='k', ec='none')
ax.add_patch(pp)
ax.autoscale_view(True,True,True)
ax.set_aspect('equal')
plt.show()
Instead of using matplotlib I'd offer a solution using SVGs, where your script only prints out the respective SVG commands.
Note: The SVG header created is missing same definitions, that's why the resulting image can not be handled by some programs. Inkscape though can open it without problems and save it again.
Defining the polygon in SVG
The solution is describe is based in the "arrow" example you showed, i.e. the lower one.
I coded the arrow as a SVG path with four points p0,p1,p2 and p3, where p0 is the upper tip, p1 is the lower right edge, p3 is the lower left one and p2 is the point underneath the tip. Every point has an x and y coordinate (p0x,p0y...).
Note: SVG coordinates unlike mathematical ones increases from left to right (x) and top-down (y), so the origin is the top-left corner.
The path is stored as string with the points being varibale. The final string is created using python's str.format() method.
Code
# width of the complete picture
width=600
# height of the complete picture
height=600
# desired recursion depth (>=1)
n=5
# "shape factor" of the arrow (=(p1y-p2y)/(p2y-p0y))
# a=0 would result in a triangle
a=0.3
def create_arrows(n, depth=1, width=600, height=600, refx=None, refy=None):
global a
if refx==None or refy==None:
# the first polygon instances defines it's reference point
# following instances are given a reference point by the caller
refx = (width - width/n)/2
refy = 0
if depth==1:
# the first polygon instance defines the size of all instancens
width=width/n
height=height/n
# the SVG definition of the polygon
polyg = r'<path d="M{p0x} {p0y} L{p1x} {p1y} L{p2x} {p2y} L{p3x} {p3y} Z" />'
points = {"p0x":refx+width/2, "p0y":refy, "p1x":refx+width, "p1y":refy+height, "p2x":refx+width/2, "p2y":refy+(1-a)*height, "p3x":refx, "p3y":refy+height}
# create the requested polygon
polygons = [polyg.format(**points)]
# if maximum recursion depth not reached call method recursively
if depth<n:
polygons.extend(myfunction(n, depth+1, width=width, height=height, refy=refy+(1-a)*height, refx=refx)) # down shifted
polygons.extend(myfunction(n, depth+1, width=width, height=height, refy=refy+height, refx=refx-width/2)) # bottom left
polygons.extend(myfunction(n, depth+1, width=width, height=height, refy=refy+height, refx=refx+width/2)) #bottom right
return(polygons)
print('<svg height="{height}" width="{width}">'.format(width=width, height=height))
# converting the list to a set is necessary since some polygons can be created by multiple recursion paths
print('\n'.join(set(create_arrows(4,width=width,height=height))))
print('</svg>')
Explanation
The arrows are created in a recursive fashion, i.e. you start with the top arrow. The dimensions of the arrows are width/n and height/n, where n is the desired recursion level (n>=1). The examples you showed would be n=1, n=2, and n=3. The below explained recursion pattern was empirically derived and is not directly based on your 3D example.
n=1: In level n=1 you're done after creating the top arrow.
n=2: In level n=2 this TOP arrow creates another three, one right below and each left and right below, respectively. The tips (p0) of these three arrows are at p2, p3 and p1 of the original arrow, respectively. You're done.
n=3: Repeat the above procedure for every arrow created in level n=2 and so on.
Below please find an example for level n=6.
For the triangles, your upper example, this idea can easily be adapted. You just have to change the polygon path and recursion pattern.
Btw. a rotated version of your trianlges are created using the given script with a=0. So if you're lazy just use that and turn the resulting SVG in inkscape.