Les Misérables

This Jupyter notebook provides an example of using the Python package gravis. The .ipynb file can be found here.

It visualizes a graph of character co-occurence in the novel Les Misérables by Victor Hugo, which is an often used example in libraries dealing with graphs or networks. Here it demonstrates the use of network analysis algorithms to generate graph annotations, which in turn have effect on visual elements of the plot. It is also an example of displaying images inside nodes.

References

  • Wikipedia

    • Les Misérables: “a French historical novel by Victor Hugo, first published in 1862, one of the greatest novels of the 19th century”

  • Donald Knuth

    • The Stanford GraphBase: “Since the data files were prepared by hand, they are subject to human error. They should therefore not be considered to be definite sources of facts, which are correctible like an article in the Wikipedia. They are intended simply as forever-frozen examples of typical data that is more or less accurate. In particular, I recently learned that I forgot to include any connection between Fantine and her infant daughter Cosette, when I summarized the encounters between the characters of Les Misérables in the data file jean.dat.”

  • NetworkX

  • graph-tool

  • d3.js

  • vis.js

[1]:
import gravis as gv
import networkx as nx

Create the graph

[2]:
graph = nx.les_miserables_graph()

Add annotations

[3]:
def detect_communities(graph, num_communities):
    community_generator = nx.algorithms.community.girvan_newman(graph)
    for i in range(num_communities-1):
        communities = next(community_generator)
    return communities


def assign_node_color_by_community(graph, communities, colors=None):
    if colors is None:
        colors = ['blue', 'orange', 'green', 'red', 'darkviolet',
                  'brown', 'pink', 'gray', 'yellowgreen', 'lightblue']
    for community_number, community in enumerate(communities):
        for member in community:
            graph.nodes[member]['color'] = colors[community_number % len(colors)]
    return graph


def assign_node_position_by_community(graph, communities):
    x_shift = -450
    y_shift = -300
    for community_number, community in enumerate(communities):
        sorted_community_members = sorted(list(community), key=lambda name: graph.nodes[name]['size'])
        for member_number, member in enumerate(sorted_community_members):
            graph.nodes[member]['x'] = x_shift + member_number * 65
            graph.nodes[member]['y'] = y_shift + community_number * 65
            graph.nodes[member]['z'] = 0
    return graph


def assign_node_size_by_degree(graph):
    for node_id in graph.nodes:
        graph.nodes[node_id]['size'] = 5 + graph.degree[node_id]
    return graph


def assign_edge_size_by_centrality(graph):
    edge_centralities = nx.algorithms.centrality.edge_betweenness_centrality(graph)
    for edge_id, centrality_value in edge_centralities.items():
        graph.edges[edge_id]['size'] = 0.25 + centrality_value * 50.0
    return graph


def assign_edge_color_by_node_colors(graph):
    for edge_id in graph.edges:
        edge = graph.edges[edge_id]
        source = graph.nodes[edge_id[0]]
        target = graph.nodes[edge_id[1]]
        edge['color'] = source['color'] if source['color'] == target['color'] else 'gray'
    return graph


def assign_node_image_by_urls(graph):
    base = 'https://upload.wikimedia.org/wikipedia/commons/'
    mapping = [
        ('Valjean', base + 'thumb/f/fd/Monsieur_Madeleine_par_Gustave_Brion.jpg/167px-Monsieur_Madeleine_par_Gustave_Brion.jpg'),
        ('Javert', base + 'thumb/7/73/Javert.jpg/162px-Javert.jpg'),
        ('Fantine', base + 'thumb/6/69/%C3%89mile_Bayard_-_Il_lui_ferma_les_yeux.jpg/179px-%C3%89mile_Bayard_-_Il_lui_ferma_les_yeux.jpg'),
        ('Cosette', base + '9/99/Ebcosette.jpg'),
        ('Marius', base + 'thumb/b/b6/Marius_sees_Cosette.jpg/170px-Marius_sees_Cosette.jpg'),
        ('Enjolras', base + 'thumb/b/b4/Friends_of_the_ABC.jpg/170px-Friends_of_the_ABC.jpg'),
        ('Eponine', base + 'thumb/a/a3/Death_of_Eponine_-_Les_Miserables.jpg/142px-Death_of_Eponine_-_Les_Miserables.jpg'),
        ('Gavroche', base + 'thumb/e/e6/Gavroche_%28Les_Mis%C3%A9rables%29.jpg/162px-Gavroche_%28Les_Mis%C3%A9rables%29.jpg'),
        ('Thenardier', base + 'thumb/3/35/Thenard.jpg/199px-Thenard.jpg'),
        ('Myriel', base + 'thumb/a/a9/Mgr_Bienvenu_par_Gustave_Brion.jpg/144px-Mgr_Bienvenu_par_Gustave_Brion.jpg'),
    ]
    for name, url in mapping:
        conv = gv.convert.image_to_data_url(url)
        graph.nodes[name]['image'] = conv
    return graph


def assign_node_hover_messages(graph):
    base = ('<span style="color:$color;font-style:italic">{}</span>'
            '<br><img src="$image" style="margin:10px;max-height:150px;max-width:150px;"></img>')
    graph.nodes['Valjean']['hover'] = base.format('Valjean')
    graph.nodes['Javert']['hover'] = base.format('Javert')
    graph.nodes['Fantine']['hover'] = base.format('Fantine')
    graph.nodes['Cosette']['hover'] = base.format('Cosette')
    graph.nodes['Marius']['hover'] = base.format('Marius')
    graph.nodes['Enjolras']['hover'] = base.format('Enjolras')
    graph.nodes['Eponine']['hover'] = base.format('Eponine')
    graph.nodes['Gavroche']['hover'] = base.format('Gavroche')
    graph.nodes['Thenardier']['hover'] = base.format('Thenardier')
    graph.nodes['Myriel']['hover'] = base.format('Myriel')
    return graph
[4]:
# Size from centrality calculation
graph = assign_node_size_by_degree(graph)
graph = assign_edge_size_by_centrality(graph)

# Color and position from community detection
communities = detect_communities(graph, 11)
graph = assign_node_color_by_community(graph, communities)
graph = assign_edge_color_by_node_colors(graph)
graph = assign_node_position_by_community(graph, communities)

# Images and hover messages from data on the web
graph = assign_node_image_by_urls(graph)
graph = assign_node_hover_messages(graph)

# Click messages from using $ syntax to use information from other properties
graph.graph['node_click'] = (
    '<ul>'
    ' <li>Id:           $id</li>'
    ' <li>Label:        $label</li>'
    ' <li>Color:        $color</li>'
    ' <li>Opacity:      $opacity</li>'
    ' <li>Size:         $size</li>'
    ' <li>Shape:        $shape</li>'
    ' <li>Border color: $border_color</li>'
    ' <li>Border size:  $border_size</li>'
    ' <li>Label color:  $label_color</li>'
    ' <li>Label size:   $label_size</li>'
    ' <li>Hover:        $hover</li>'
    ' <li>x:            $x</li>'
    ' <li>y:            $y</li>'
    '</ul>'
)

graph.graph['edge_click'] = (
    '<ul>'
    ' <li>Id:           $id</li>'
    ' <li>Label:        $label</li>'
    ' <li>Color:        $color</li>'
    ' <li>Opacity:      $opacity</li>'
    ' <li>Size:         $size</li>'
    ' <li>Label color:  $label_color</li>'
    ' <li>Label size:   $label_size</li>'
    ' <li>Hover:        $hover</li>'
    '</ul>'
)

# General options
graph.graph['node_border_size'] = 2
graph.graph['node_border_color'] = 'white'
graph.graph['edge_color'] = 'black'
graph.graph['edge_opacity'] = 0.9

Plot the annotated graph

[5]:
gv.d3(graph, node_image_size_factor=2.5)
[5]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Edge label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Rotation
Angle
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Edge labels
Visibility
Size
Scaling factor
Rotation
Angle
Layout algorithm
Simulation
Many-body force
Strength
Theta
Min
Max
Links force
Collision force
Radius
Strength
x-positioning force
Strength
y-positioning force
Strength
Centering force
[6]:
gv.vis(graph, node_image_size_factor=2.5)
[6]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Edge label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Rotation
Angle
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Edge labels
Visibility
Size
Scaling factor
Rotation
Angle
Layout algorithm
Simulation
Algorithm
Parameters
Gravitational constant
Spring length
Spring constant
Avoid overlap
Central gravity
[7]:
gv.three(graph, node_image_size_factor=1.5)
[7]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Layout algorithm
Simulation
Many-body force
Strength
Theta
Min
Max
Links force
x-positioning force
Strength
y-positioning force
Strength
z-positioning force
Strength
Centering force