Arc Diagram of Star Wars Characters that Interacted in The Force Awakens

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())


dict_keys(['nodes', 'links'])

In [4]:
import numpy as np

In [5]:
L = len(data['nodes'])# number of nodes (characters)
data['nodes'][0]


Out[5]:
{'name': 'LUKE', 'value': 2, 'colour': '#3881e5'}

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])


{'source': 2, 'target': 1, 'value': 1}

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 [ ]: