ToferC ToferC - 1 year ago 87
Python Question

Set dynamic node shape in network with matplotlib

First time poster here, so please be gentle. :)

I'm trying to graph a network of characters of different types in Networkx and want to set different node shapes for each type. For example, I'd like characters to be circles, creatures to be triangles, etc. I've tried to figure this out for several hours and have searched SO extensively, but I haven't found a way to achieve this other than to set different node_lists for each type of character and render them separately, which just seems counterintuitive.

The issue is that I'm unable to access the node_shape dictionary value from within:

nx.draw_networkx_nodes(G, pos)

I've tried multiple solutions including trying to access the node attribute, creating an external dictionary or list and accessing it from within the call, setting up a list comprehension or iterator and nothing seems to work.

Either I pass a list, which is pulled in wholesale, a dictionary, which the function isn't able to hash, or an instance of the list such as
, in which case the function only takes the first value and applies it to all nodes.

I am able to set color by creating a separate node_colors list which is iterated over by the function and even tried creating a dictionary so that the node_shape is triggered by node_color, but that didn't work either.

I'm hoping to use the code as an add-on to a web app developed in Python 3.4 and Django 1.8, so Graphviz isn't an option.

Thanks in advance for any assistance or reference to alternate libraries.

Here is my code:

import json
import requests
import networkx as nx
import matplotlib.pyplot as plt

personas = ''
target = requests.get(personas)
x = target.json()

story_objects = {}
labels = {}
node_colors = []

for character in x:
name = character["name"]
story = character["story"]
c_type = character["c_type"]
story_objects[name] = {}
story_objects[name]['name'] = name
story_objects[name]['story'] = story
story_objects[name]['c_type'] = c_type
story_objects[name]['to_relationships'] = []
if character['c_type'] == "Character":
story_objects[name]['node_shape'] = 'o'
story_objects[name]['node_color'] = 'r'
elif character['c_type'] == "Organization":
story_objects[name]['node_shape'] = 'h'
story_objects[name]['node_color'] = 'b'
elif character['c_type'] == "Creature":
story_objects[name]['node_shape'] = '^'
story_objects[name]['node_color'] = 'g'
elif character['c_type'] == "Force":
story_objects[name]['node_shape'] = 'v'
story_objects[name]['node_color'] = 'c'
elif character['c_type'] == "Thing":
story_objects[name]['node_shape'] = 's'
story_objects[name]['node_color'] = 'y'

for relationship in character["to_relationships"]:
break_1 = relationship.find(">>")
break_2 = relationship.find("weight:")
sub_1 = relationship[0:break_1].strip()
context = relationship[break_1:break_2]
weight = relationship[break_2+8:-1]
story_objects[name]['to_relationships'].append([sub_1, context, weight])


for sub in story_objects:
s = story_objects[sub]
if s['story'] == "":
G.add_node(s['name'], node_shape=s['node_shape'])
labels[s['name']] = s['name']


print("***", s['name'], "***", s['c_type'])
print("details:", s['node_color'], s['node_shape'])
for i in s['to_relationships']:
print('target:', i[0])
print('context:', i[1])
print('weight:', i[2])
G.add_edge(s['name'], i[0], weight=int(i[2]))

node_shapes=nx.get_node_attributes(G, 'node_shape') # Latest attempt at getting this to work
node_shapes = [v for k,v in node_shapes.items()]


nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_shape=node_shapes.pop(0)) # <--- This is where I'm having problems
nx.draw_networkx_edges(G, pos)
nx.draw_networkx_labels(G, pos, labels)

Answer Source

I am afraid that this would have to be done using multiple passes.

The main idea is to use a layout to get the positions of the nodes and then use draw_networkx_nodes repeatedly for the n different classes of nodes.

For example:

import networkx
import pylab

#Build a graph (Node attribute 's' determines the node shape here)
G = networkx.Graph()
G.add_node(0, s="^", b=1)
G.add_node(1, s="^", b=2)

G.add_node(2, s="o", b=3)
G.add_node(3, s="o", b=4)

G.add_node(4, s="v", b=5)
G.add_node(5, s="v", b=6)


#Drawing the graph
#First obtain the node positions using one of the layouts
nodePos = networkx.layout.spring_layout(G)

#The rest of the code here attempts to automate the whole process by
#first determining how many different node classes (according to
#attribute 's') exist in the node set and then repeatedly calling 
#draw_networkx_node for each. Perhaps this part can be optimised further.

#Get all distinct node classes according to the node shape attribute
nodeShapes = set((aShape[1]["s"] for aShape in G.nodes(data = True)))

#For each node class...
for aShape in nodeShapes:
    #...filter and draw the subset of nodes with the same symbol in the positions that are now known through the use of the layout.
    networkx.draw_networkx_nodes(G,nodePos,node_shape = aShape, nodelist = [sNode[0] for sNode in filter(lambda x: x[1]["s"]==aShape,G.nodes(data = True))])

#Finally, draw the edges between the nodes

#And show the final result

Final result looks something like this:

enter image description here

Hope this helps.