Arc diagram is a special layout for graphs. It places the nodes on a line and the links are arcs of circles. Here we plot the arc diagram associated to characters in Stars Wars: The Force Awakens. The nodes are characters. They are colored according to the number of times the corresponding character spoke across the 7th episode of Star Wars.
In [1]:
url="https://raw.githubusercontent.com/evelinag/StarWars-social-network/master/"+\
"networks/starwars-episode-7-interactions-allCharacters.json"
In [2]:
import requests
import json
r = requests.get(url)
data = json.loads(r.content)
In [3]:
print (data.keys())
In [4]:
import numpy as np
In [5]:
L = len(data['nodes'])# number of nodes (characters)
data['nodes'][0]
Out[5]:
In [6]:
labels = [item['name'].lower().title() for item in data['nodes']]
values = [item['value'] for item in data['nodes']]
hover_text = [f'{labels[k]}, {values[k]} scenes' for k in range(L)]
In [7]:
print(data['links'][0])
Define the graph edges, i.e. the pairs of nodes that are linked:
In [8]:
edges = [(item['source'], item['target']) for item in data['links']] #Edges
In [9]:
interact_strength = [item['value'] for item in data['links']]
keys = sorted(set(interact_strength))
Associate to each link a width value for the corresponding arc, depending on the number of interactions between the nodes connected by that arc:
In [10]:
widths = [0.5+k*0.25 for k in range(5)] + [2+k*0.25 for k in range(4)]+[3, 3.25, 3.75, 4.25, 5, 5.25, 7]
d = dict(zip(keys, widths))
nwidths = [d[val] for val in interact_strength]
Below we define a few functions that compute points on arcs of circles connecting two nodes/characters. These arcs are defined as quadratic rational Bezier curves ([Gallier]), of control points $b_0, b_1, b_2$.
$b_0$ and $b_2$ are the points on the x-axis, representing two connected nodes. $b_1$ is chosen such that the triangle $\Delta b_0b_1b_2$ is equilateral. The weights of the control points are respectively (1, 0.5, 1).
In [11]:
def get_b1(b0, b2):
# b0, b1 list of x, y coordinates
if len(b0) != len(b2) != 2:
raise ValueError('b0, b1 must be lists of two elements')
b1 = 0.5 * (np.asarray(b0)+np.asarray(b2))+\
0.5 * np.array([0,1.0]) * np.sqrt(3) * np.linalg.norm(np.array(b2)-np.array(b0))
return b1.tolist()
In [12]:
def dim_plus_1(b, w):#lift the points b0, b1, b2 to 3D points a0, a1, a2 (see Gallier book)
#b is a list of 3 lists of 2D points, i.e. a list of three 2-lists
#w is a list of numbers (weights) of len equal to the len of b
if not isinstance(b, list) or not isinstance(b[0], list):
raise ValueError('b must be a list of three 2-lists')
if len(b) != len(w) != 3:
raise ValueError('the number of weights must be equal to the nr of points')
else:
a = np.array([point + [w[i]] for (i, point) in enumerate(b)])
a[1, :2] *= w[1]
return a
In [13]:
def Bezier_curve(bz, nr): #the control point coordinates are passed in a list bz=[bz0, bz1, bz2]
# bz is a list of three 2-lists
# nr is the number of points to be computed on each arc
t = np.linspace(0, 1, nr)
#for each parameter t[i] evaluate a point on the Bezier curve with the de Casteljau algorithm
N = len(bz)
points = [] # the list of points to be computed on the Bezier curve
for i in range(nr):
aa = np.copy(bz)
for r in range(1, N):
aa[:N-r,:] = (1-t[i]) * aa[:N-r,:] + t[i] * aa[1:N-r+1,:] # convex combination of points
points.append(aa[0,:])
return np.array(points)
The points of the 3D-Bezier curve computed from the lifted control points $b_0, b_1, b_2$ are projected through a central projection of center $O(0,0,0)$ onto the plane $z=1$, to get the 2D rational Bezier curve, i.e. an arc of circle whose tangents at the nodes $b_0, b_2$, form an angle of $\pi/3$ with the line $b_0b_2$:
In [14]:
def Rational_Bezier_curve(a, nr):
discrete_curve = Bezier_curve(a, nr )
return [p[:2]/p[2] for p in discrete_curve]
In [15]:
import plotly.plotly as py
import plotly.graph_objs as go
Define the trace for nodes placed on the x-axis. The nodes are colored with the colorscale pl_density
:
In [16]:
pl_density = [[0.0, 'rgb(230,240,240)'],
[0.1, 'rgb(187,220,228)'],
[0.2, 'rgb(149,197,226)'],
[0.3, 'rgb(123,173,227)'],
[0.4, 'rgb(115,144,227)'],
[0.5, 'rgb(119,113,213)'],
[0.6, 'rgb(120,84,186)'],
[0.7, 'rgb(115,57,151)'],
[0.8, 'rgb(103,35,112)'],
[0.9, 'rgb(82,20,69)'],
[1.0, 'rgb(54,14,36)']]
In [17]:
node_trace = dict(type='scatter',
x=list(range(L)),
y=[0]*L,
mode='markers',
marker=dict(size=12,
color=values,
colorscale=pl_density,
showscale=False,
line=dict(color='rgb(50,50,50)', width=0.75)),
text=hover_text,
hoverinfo='text')
In [18]:
data = []
tooltips = [] #list of strings to be displayed when hovering the mouse over the middle of the circle arcs
xx = []
yy = []
Generate the arcs of circles as rational Bezier curves and append each arc to a list:
In [19]:
X = list(range(L)) # node x-coordinates
nr = 75
for i, (j, k) in enumerate(edges):
if j < k:
tooltips.append(f'interactions({labels[j]}, {labels[k]})={interact_strength[i]}')
else:
tooltips.append(f'interactions({labels[k]}, {labels[j]})={interact_strength[i]}')
b0 = [X[j], 0.0]
b2 = [X[k], 0.0]
b1 = get_b1(b0, b2)
a = dim_plus_1([b0, b1, b2], [1, 0.5, 1])
pts = Rational_Bezier_curve(a, nr)
xx.append(pts[nr//2][0]) #abscissa of the middle point on the computed arc
yy.append(pts[nr//2][1]) #ordinate of the same point
x,y = zip(*pts)
data.append(dict(type='scatter',
x=x,
y=y,
name='',
mode='lines',
line=dict(width=nwidths[i], color='#6b8aca', shape='spline'),
hoverinfo='none'
)
)
In [20]:
data.append(dict(type='scatter',
x=xx,
y=yy,
name='',
mode='markers',
marker=dict(size=0.5, color='#6b8aca'),
text=tooltips,
hoverinfo='text'))
data.append(node_trace)
In [21]:
title = "Arc Diagram of Star Wars Characters that Interacted in The Force Awakens"
anno_text = "Data source: "+\
"<a href='https://github.com/evelinag/StarWars-social-network/tree/master/networks'> [1]</a>"
In [28]:
layout = dict(
title=title,
font=dict(size=10),
width=900,
height=460,
showlegend=False,
xaxis=dict(anchor='y',
showline=False,
zeroline=False,
showgrid=False,
tickvals=list(range(27)),
ticktext=labels,
tickangle=50,
),
yaxis=dict(visible=False),
hovermode='closest',
margin=dict(t=80, b=110, l=10, r=10),
annotations=[dict(showarrow=False,
text=anno_text,
xref='paper',
yref='paper',
x=0.05,
y=-0.3,
xanchor='left',
yanchor='bottom',
font=dict(size=11 ))
]
)
fig = go.FigureWidget(data=data, layout=layout)
#py.plot(fig, filename='arc-diagram-FA') # plot online
In [30]:
fig
The online plot is posted at the URL https://plot.ly/~empet/13574.
In [ ]: