{ "cells": [ { "cell_type": "markdown", "id": "bd6f719c", "metadata": {}, "source": [ "# Les Misérables\n", "\n", "This Jupyter notebook provides an example of using the Python package [gravis](https://pypi.org/project/gravis). The .ipynb file can be found [here](https://github.com/robert-haas/gravis/tree/master/examples).\n", "\n", "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**.\n", "\n", "\n", "## References\n", "\n", "- Wikipedia\n", " - [Les Misérables](https://en.wikipedia.org/wiki/Les_Mis%C3%A9rables): \"a French historical novel by Victor Hugo, first published in 1862, one of the greatest novels of the 19th century\"\n", "- Donald Knuth\n", " - [The Stanford GraphBase](https://www-cs-faculty.stanford.edu/~knuth/sgb.html): \"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.\"\n", "- NetworkX\n", " - [les_miserables_graph()](https://networkx.org/documentation/stable/reference/generated/networkx.generators.social.les_miserables_graph.html) function returning the Les Misérables graph\n", "- graph-tool\n", " - [Dataset collection](https://graph-tool.skewed.de/static/doc/collection.html) with Les Misérables graph\n", "- d3.js\n", " - [Force-directed graph](https://observablehq.com/@d3/force-directed-graph) of Les Misérables graph\n", " - [Adjacency matrix representation](https://bost.ocks.org/mike/miserables/) of Les Misérables graph\n", "- vis.js\n", " - [Network visualization](https://visjs.github.io/vis-network/examples/network/exampleApplications/lesMiserables.html) of Les Misérables graph" ] }, { "cell_type": "code", "execution_count": null, "id": "f1db297e", "metadata": {}, "outputs": [], "source": [ "import gravis as gv\n", "import networkx as nx" ] }, { "cell_type": "markdown", "id": "ff474a36", "metadata": {}, "source": [ "## Create the graph" ] }, { "cell_type": "code", "execution_count": null, "id": "27320897", "metadata": {}, "outputs": [], "source": [ "graph = nx.les_miserables_graph()" ] }, { "cell_type": "markdown", "id": "a757084c", "metadata": {}, "source": [ "## Add annotations" ] }, { "cell_type": "code", "execution_count": null, "id": "1a2aa668", "metadata": {}, "outputs": [], "source": [ "def detect_communities(graph, num_communities):\n", " community_generator = nx.algorithms.community.girvan_newman(graph)\n", " for i in range(num_communities-1):\n", " communities = next(community_generator)\n", " return communities\n", "\n", "\n", "def assign_node_color_by_community(graph, communities, colors=None):\n", " if colors is None:\n", " colors = ['blue', 'orange', 'green', 'red', 'darkviolet',\n", " 'brown', 'pink', 'gray', 'yellowgreen', 'lightblue']\n", " for community_number, community in enumerate(communities):\n", " for member in community:\n", " graph.nodes[member]['color'] = colors[community_number % len(colors)]\n", " return graph\n", "\n", "\n", "def assign_node_position_by_community(graph, communities):\n", " x_shift = -450\n", " y_shift = -300\n", " for community_number, community in enumerate(communities):\n", " sorted_community_members = sorted(list(community), key=lambda name: graph.nodes[name]['size'])\n", " for member_number, member in enumerate(sorted_community_members):\n", " graph.nodes[member]['x'] = x_shift + member_number * 65\n", " graph.nodes[member]['y'] = y_shift + community_number * 65\n", " graph.nodes[member]['z'] = 0\n", " return graph\n", "\n", "\n", "def assign_node_size_by_degree(graph):\n", " for node_id in graph.nodes:\n", " graph.nodes[node_id]['size'] = 5 + graph.degree[node_id]\n", " return graph\n", "\n", "\n", "def assign_edge_size_by_centrality(graph):\n", " edge_centralities = nx.algorithms.centrality.edge_betweenness_centrality(graph)\n", " for edge_id, centrality_value in edge_centralities.items():\n", " graph.edges[edge_id]['size'] = 0.25 + centrality_value * 50.0\n", " return graph\n", "\n", "\n", "def assign_edge_color_by_node_colors(graph):\n", " for edge_id in graph.edges:\n", " edge = graph.edges[edge_id]\n", " source = graph.nodes[edge_id[0]]\n", " target = graph.nodes[edge_id[1]]\n", " edge['color'] = source['color'] if source['color'] == target['color'] else 'gray'\n", " return graph\n", "\n", "\n", "def assign_node_image_by_urls(graph):\n", " base = 'https://upload.wikimedia.org/wikipedia/commons/'\n", " mapping = [\n", " ('Valjean', base + 'thumb/f/fd/Monsieur_Madeleine_par_Gustave_Brion.jpg/167px-Monsieur_Madeleine_par_Gustave_Brion.jpg'),\n", " ('Javert', base + 'thumb/7/73/Javert.jpg/162px-Javert.jpg'),\n", " ('Fantine', base + 'thumb/6/69/%C3%89mile_Bayard_-_Il_lui_ferma_les_yeux.jpg/179px-%C3%89mile_Bayard_-_Il_lui_ferma_les_yeux.jpg'),\n", " ('Cosette', base + '9/99/Ebcosette.jpg'),\n", " ('Marius', base + 'thumb/b/b6/Marius_sees_Cosette.jpg/170px-Marius_sees_Cosette.jpg'),\n", " ('Enjolras', base + 'thumb/b/b4/Friends_of_the_ABC.jpg/170px-Friends_of_the_ABC.jpg'),\n", " ('Eponine', base + 'thumb/a/a3/Death_of_Eponine_-_Les_Miserables.jpg/142px-Death_of_Eponine_-_Les_Miserables.jpg'),\n", " ('Gavroche', base + 'thumb/e/e6/Gavroche_%28Les_Mis%C3%A9rables%29.jpg/162px-Gavroche_%28Les_Mis%C3%A9rables%29.jpg'),\n", " ('Thenardier', base + 'thumb/3/35/Thenard.jpg/199px-Thenard.jpg'),\n", " ('Myriel', base + 'thumb/a/a9/Mgr_Bienvenu_par_Gustave_Brion.jpg/144px-Mgr_Bienvenu_par_Gustave_Brion.jpg'),\n", " ]\n", " for name, url in mapping:\n", " conv = gv.convert.image_to_data_url(url)\n", " graph.nodes[name]['image'] = conv\n", " return graph\n", "\n", "\n", "def assign_node_hover_messages(graph):\n", " base = ('{}'\n", " '
')\n", " graph.nodes['Valjean']['hover'] = base.format('Valjean')\n", " graph.nodes['Javert']['hover'] = base.format('Javert')\n", " graph.nodes['Fantine']['hover'] = base.format('Fantine')\n", " graph.nodes['Cosette']['hover'] = base.format('Cosette')\n", " graph.nodes['Marius']['hover'] = base.format('Marius')\n", " graph.nodes['Enjolras']['hover'] = base.format('Enjolras') \n", " graph.nodes['Eponine']['hover'] = base.format('Eponine') \n", " graph.nodes['Gavroche']['hover'] = base.format('Gavroche') \n", " graph.nodes['Thenardier']['hover'] = base.format('Thenardier')\n", " graph.nodes['Myriel']['hover'] = base.format('Myriel')\n", " return graph" ] }, { "cell_type": "code", "execution_count": null, "id": "390a6253", "metadata": {}, "outputs": [], "source": [ "# Size from centrality calculation\n", "graph = assign_node_size_by_degree(graph)\n", "graph = assign_edge_size_by_centrality(graph)\n", "\n", "# Color and position from community detection\n", "communities = detect_communities(graph, 11)\n", "graph = assign_node_color_by_community(graph, communities)\n", "graph = assign_edge_color_by_node_colors(graph)\n", "graph = assign_node_position_by_community(graph, communities)\n", "\n", "# Images and hover messages from data on the web\n", "graph = assign_node_image_by_urls(graph)\n", "graph = assign_node_hover_messages(graph)\n", "\n", "# Click messages from using $ syntax to use information from other properties\n", "graph.graph['node_click'] = (\n", " ''\n", ")\n", "\n", "graph.graph['edge_click'] = (\n", " ''\n", ")\n", "\n", "# General options\n", "graph.graph['node_border_size'] = 2\n", "graph.graph['node_border_color'] = 'white'\n", "graph.graph['edge_color'] = 'black'\n", "graph.graph['edge_opacity'] = 0.9" ] }, { "cell_type": "markdown", "id": "22f7bb7c", "metadata": {}, "source": [ "## Plot the annotated graph" ] }, { "cell_type": "code", "execution_count": null, "id": "c96922aa", "metadata": { "scrolled": false }, "outputs": [], "source": [ "gv.d3(graph, node_image_size_factor=2.5)" ] }, { "cell_type": "code", "execution_count": null, "id": "92297023", "metadata": {}, "outputs": [], "source": [ "gv.vis(graph, node_image_size_factor=2.5)" ] }, { "cell_type": "code", "execution_count": null, "id": "01c86ed2", "metadata": {}, "outputs": [], "source": [ "gv.three(graph, node_image_size_factor=1.5)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.12" } }, "nbformat": 4, "nbformat_minor": 5 }