{ "cells": [ { "cell_type": "markdown", "id": "cc191d89", "metadata": {}, "source": [ "# Advanced use\n", "\n", "This Jupyter notebook demonstrates some advanced features of 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)." ] }, { "cell_type": "code", "execution_count": 1, "id": "51a0be7d", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "import gravis as gv\n", "import networkx as nx" ] }, { "cell_type": "markdown", "id": "cb14b299", "metadata": {}, "source": [ "## Define a graph in all possible ways\n", "\n", "The following gives an overview of all ways that are available for defining a graph that is recognized by gravis." ] }, { "cell_type": "markdown", "id": "18d07cc4", "metadata": {}, "source": [ "### 1) String in gJGF" ] }, { "cell_type": "code", "execution_count": null, "id": "73643d6a", "metadata": {}, "outputs": [], "source": [ "graph1 = \"\"\"\n", "{\n", " \"graph\": {\n", " \"metadata\": {\"node_color\": \"blue\"},\n", " \"nodes\": [{\"id\": \"0\"}, {\"id\": \"1\"}, {\"id\": \"2\"}],\n", " \"edges\": [{\"source\": 0, \"target\": 1}, {\"source\": 1, \"target\": 2}, {\"source\": 2, \"target\": 0}]\n", " }\n", "}\n", "\"\"\"\n", "\n", "gv.d3(graph1, graph_height=100)" ] }, { "cell_type": "markdown", "id": "a224d5da", "metadata": {}, "source": [ "### 2) Dictionary in gJGF" ] }, { "cell_type": "markdown", "id": "fa9fab63", "metadata": {}, "source": [ "The Python dictionary needs to be equivalent to a JSON string in gJGF. The following demonstrates two [different ways a dictionary can be created in Python](https://docs.python.org/3/library/stdtypes.html#typesmapping)." ] }, { "cell_type": "code", "execution_count": null, "id": "6fda4ff0", "metadata": {}, "outputs": [], "source": [ "graph2 = {\n", " 'graph': {\n", " 'metadata': {'node_color': 'orange'},\n", " 'nodes': [{'id': '0'}, {'id': '1'}, {'id': '2'}],\n", " 'edges': [{'source': 0, 'target': 1}, {'source': 1, 'target': 2}, {'source': 2, 'target': 0}],\n", " }\n", "}\n", "\n", "gv.d3(graph2, graph_height=100)" ] }, { "cell_type": "code", "execution_count": null, "id": "23ade5af", "metadata": {}, "outputs": [], "source": [ "graph3 = dict(graph=dict(\n", " metadata=dict(node_color='red'),\n", " nodes=[dict(id=0), dict(id=1), dict(id=2)],\n", " edges=[dict(source=0, target=1), dict(source=1, target=2), dict(source=2, target=0)],\n", "))\n", "\n", "gv.d3(graph3, graph_height=100)" ] }, { "cell_type": "markdown", "id": "17ae0e0a", "metadata": {}, "source": [ "### 3) Text file in gJGF\n", "\n", "The text file needs to contain a string in gJGF." ] }, { "cell_type": "code", "execution_count": null, "id": "a54c7235", "metadata": {}, "outputs": [], "source": [ "graph4 = os.path.join('data', 'graph_basic.gjgf')\n", "\n", "gv.d3(graph4, graph_height=100)" ] }, { "cell_type": "markdown", "id": "40b49b01", "metadata": {}, "source": [ "### 4) Graph object from external library\n", "\n", "The following demonstrates [two ways](https://networkx.org/documentation/stable/tutorial.html) in which a graph can be defined in [NetworkX](https://networkx.org). Other libraries are also supported an shown in other examples." ] }, { "cell_type": "code", "execution_count": null, "id": "bcf8523b", "metadata": {}, "outputs": [], "source": [ "graph5 = nx.Graph()\n", "graph5.graph['node_color'] = 'green'\n", "graph5.add_node(0)\n", "graph5.add_node(1)\n", "graph5.add_node(2)\n", "graph5.add_edge(0, 1)\n", "graph5.add_edge(1, 2)\n", "graph5.add_edge(2, 0)\n", "\n", "gv.d3(graph5, graph_height=100)" ] }, { "cell_type": "code", "execution_count": null, "id": "2f48ff8e", "metadata": {}, "outputs": [], "source": [ "graph6 = nx.Graph()\n", "graph6.graph['node_color'] = 'cyan'\n", "graph6.add_edges_from([(0, 1), (1, 2), (2, 0)])\n", "\n", "gv.d3(graph6, graph_height=100)" ] }, { "cell_type": "markdown", "id": "2d4ab96a", "metadata": {}, "source": [ "Note that a graph object is internally converted into gJGF with a function that is also available to users" ] }, { "cell_type": "code", "execution_count": null, "id": "deaa1e7c", "metadata": {}, "outputs": [], "source": [ "dictionary = gv.convert.any_to_gjgf(graph6)\n", "dictionary" ] }, { "cell_type": "code", "execution_count": null, "id": "0d9d6b5b", "metadata": {}, "outputs": [], "source": [ "import json\n", "\n", "json_string = json.dumps(dictionary, indent=2)\n", "print(json_string)" ] }, { "cell_type": "markdown", "id": "4529fd8e", "metadata": {}, "source": [ "### 5) List of any of the previous" ] }, { "cell_type": "markdown", "id": "1cbe3776", "metadata": {}, "source": [ "Multiple graphs can be visualized in a single output cell by using the ``display`` method of a ``Figure`` object returned by a plotting function." ] }, { "cell_type": "code", "execution_count": null, "id": "d2dc8252", "metadata": {}, "outputs": [], "source": [ "graphs = [graph1, graph2, graph3, graph4, graph5, graph6]\n", "\n", "for g in graphs:\n", " print(type(g))\n", " fig = gv.d3(g, graph_height=75)\n", " fig.display(inline=True)" ] }, { "cell_type": "markdown", "id": "b9ae7809", "metadata": {}, "source": [ "Multiple graph can also be made available in a single visualization. The user can switch between them in the **Data selection** tab." ] }, { "cell_type": "code", "execution_count": null, "id": "832efd28", "metadata": {}, "outputs": [], "source": [ "gv.d3(graphs, graph_height=200)" ] }, { "cell_type": "markdown", "id": "f9a2354b", "metadata": {}, "source": [ "## Define a graph with all available annotations\n", "\n", "### 1) Using a dictionary in gJGF" ] }, { "cell_type": "code", "execution_count": null, "id": "af57e790", "metadata": {}, "outputs": [], "source": [ "graph_gjgf = {\n", " 'graph': {\n", " 'directed': True,\n", " 'metadata': {\n", " 'node_label_size': 14,\n", " 'node_label_color': 'green',\n", " 'edge_label_size': 10,\n", " 'edge_label_color': 'blue',\n", " },\n", " 'nodes': [\n", " {'id': '0', 'label': 'first node', 'metadata': {\n", " 'color': 'red',\n", " 'size': 15,\n", " 'shape': 'rectangle',\n", " 'opacity': 0.7,\n", " 'label_color': 'red',\n", " 'label_size': 20,\n", " 'border_color': 'black',\n", " 'border_size': 3,\n", " }},\n", " {'id': '1'},\n", " {'id': '2'},\n", " {'id': '3', 'metadata': {\n", " 'color': 'green',\n", " 'size': 15,\n", " 'shape': 'hexagon',\n", " 'opacity': 0.7,\n", " 'label_color': 'green',\n", " 'label_size': 10,\n", " 'border_color': 'blue',\n", " 'border_size': 3,\n", " }},\n", " {'id': '4'},\n", " {'id': '5'},\n", " {'id': '6', 'label': 'last node'},\n", " ],\n", " 'edges': [\n", " {'source': 0, 'target': 1},\n", " {'source': 1, 'target': 2, 'label': 'e2'},\n", " {'source': 2, 'target': 3},\n", " {'source': 3, 'target': 4},\n", " {'source': 4, 'target': 5, 'label': 'e5', 'metadata': {\n", " 'color': 'orange',\n", " 'label_color': 'gray',\n", " 'label_size': 14,\n", " 'size': 4.0,\n", " }},\n", " {'source': 5, 'target': 6},\n", " {'source': 6, 'target': 2, 'label': 'e2'},\n", " ]\n", " }\n", "}" ] }, { "cell_type": "code", "execution_count": null, "id": "14eb28fb", "metadata": {}, "outputs": [], "source": [ "gv.d3(graph_gjgf, graph_height=200,\n", " node_label_data_source='label',\n", " show_edge_label=True, edge_label_data_source='label')" ] }, { "cell_type": "markdown", "id": "15d0889a", "metadata": {}, "source": [ "### 2) Using NetworkX" ] }, { "cell_type": "code", "execution_count": null, "id": "2b3966e6", "metadata": {}, "outputs": [], "source": [ "# Graph and graph attributes\n", "graph = nx.DiGraph()\n", "graph.graph['node_label_size'] = 14\n", "graph.graph['node_label_color'] = 'green'\n", "graph.graph['edge_label_size'] = 10\n", "graph.graph['edge_label_color'] = 'blue'\n", "\n", "# Nodes and node attributes\n", "graph.add_node(0, label='first node', color='red', size=15, shape='rectangle', opacity=0.7,\n", " label_color='red', label_size=20, border_color='black', border_size=3)\n", "graph.add_node(3, color='green', size=15, shape='hexagon', opacity=0.7,\n", " label_color='green', label_size=10, border_color='blue', border_size=3)\n", "graph.add_node(6, label='last node')\n", "\n", "# Edges and edge attributes\n", "graph.add_edge(0, 1) # add_edge creates nodes if they don't exist yet\n", "graph.add_edge(1, 2, label='e2')\n", "graph.add_edge(2, 3)\n", "graph.add_edge(3, 4)\n", "graph.add_edge(4, 5, label='e5', color='orange', label_color='gray', label_size=14, size=4.0)\n", "graph.add_edge(5, 6)\n", "graph.add_edge(6, 2, label='e7')" ] }, { "cell_type": "code", "execution_count": null, "id": "16d3cb05", "metadata": {}, "outputs": [], "source": [ "gv.d3(graph, graph_height=200,\n", " node_label_data_source='label',\n", " show_edge_label=True, edge_label_data_source='label')" ] }, { "cell_type": "markdown", "id": "9ff65b47", "metadata": {}, "source": [ "### 3) Using a text file in gJGF" ] }, { "cell_type": "code", "execution_count": null, "id": "d95d0eff", "metadata": {}, "outputs": [], "source": [ "graph = os.path.join('data', 'graph_advanced.gjgf')\n", "\n", "gv.d3(graph, graph_height=200, node_label_data_source='label',\n", " show_edge_label=True, edge_label_data_source='label')" ] }, { "cell_type": "markdown", "id": "f6353a18", "metadata": {}, "source": [ "## Define a graph with multi-edges and self loops\n", "\n", "This graph is a depiction of a state machine. Nodes represent states, edges represent state transitions and edge labels represent inputs that cause the corresponding state transition." ] }, { "cell_type": "code", "execution_count": 2, "id": "9c28843f", "metadata": {}, "outputs": [], "source": [ "graph_gjgf = {\n", " 'graph':{\n", " 'directed': True,\n", " 'metadata': {'node_size': 20},\n", " 'nodes': {\n", " 's0': {'metadata': {'x': -150, 'y': 20, 'size': 0}},\n", " 's1': {'label': 'State 1', 'metadata': {'x': -100, 'y': 20, 'color': 'red', 'label_color': 'red'}},\n", " 's2': {'label': 'State 2', 'metadata': {'x': 0, 'y': 20, 'color': 'green', 'label_color': 'green'}},\n", " 's3': {'label': 'State 3', 'metadata': {'x': 100, 'y': 20, 'color': 'blue', 'label_color': 'blue'}},\n", " },\n", " 'edges': [\n", " {'source': 's0', 'target': 's1'},\n", " {'source': 's1', 'target': 's2', 'label': 'a'},\n", " {'source': 's1', 'target': 's2', 'label': 'b'},\n", " {'source': 's2', 'target': 's2', 'label': 'a'},\n", " {'source': 's2', 'target': 's2', 'label': 'b'},\n", " {'source': 's2', 'target': 's1', 'label': 'c'},\n", " {'source': 's2', 'target': 's1', 'label': 'd'},\n", " {'source': 's2', 'target': 's1', 'label': 'e'},\n", " {'source': 's2', 'target': 's3', 'label': 'f'},\n", " {'source': 's3', 'target': 's3', 'label': 'a'},\n", " {'source': 's3', 'target': 's3', 'label': 'b'},\n", " {'source': 's3', 'target': 's2', 'label': 'f'},\n", " ]\n", " }\n", "}" ] }, { "cell_type": "code", "execution_count": 10, "id": "ac5b6ba5", "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", " \n", "\n", "
\n", "
\n", "\n", "
\n", "
\n", "
\n", "
\n", "
\n", " Details for selected element\n", "
\n", "
\n", "
\n", "
\n", "
\n", "
\n", "\n", "
\n", "
\n", " \n", "
\n", " General\n", "
\n", "
\n", " \n", "
\n", "
\n", " App state\n", "
\n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " Display mode\n", "
\n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " Export\n", "
\n", "
\n", " \n", " \n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", " Data selection\n", "
\n", "
\n", " \n", "
\n", "
\n", " Graph\n", "
\n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " Node label text\n", "
\n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " Edge label text\n", "
\n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", " Node size\n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " Minimum\n", " \n", " \n", "
\n", "
\n", " Maximum\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", " Edge size\n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " Minimum\n", " \n", " \n", "
\n", "
\n", " Maximum\n", " \n", " \n", "
\n", "
\n", "
\n", "
\n", " \n", "
\n", " Nodes\n", "
\n", "
\n", " \n", "
\n", "
\n", " Visibility\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " Size\n", "
\n", "
\n", "
\n", " Scaling factor\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " Position\n", "
\n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " Drag behavior\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " Hover behavior\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " Node images\n", "
\n", "
\n", " \n", "
\n", "
\n", " Visibility\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " Size\n", "
\n", "
\n", " Scaling factor\n", " \n", " \n", "
\n", "
\n", "
\n", "
\n", " \n", "
\n", " Node labels\n", "
\n", "
\n", " \n", "
\n", "
\n", " Visibility\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " Size\n", "
\n", "
\n", " Scaling factor\n", " \n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " Rotation\n", "
\n", "
\n", " Angle\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", " Edges\n", "
\n", "
\n", " \n", "
\n", "
\n", " Visibility\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " Size\n", "
\n", "
\n", " Scaling factor\n", " \n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " Form\n", "
\n", "
\n", " Curvature\n", " \n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " Hover behavior\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", "
\n", " \n", "
\n", " Edge labels\n", "
\n", "
\n", " \n", "
\n", "
\n", " Visibility\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " Size\n", "
\n", "
\n", " Scaling factor\n", " \n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " Rotation\n", "
\n", "
\n", " Angle\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", " Layout algorithm\n", "
\n", "
\n", "\n", " \n", "
\n", "
\n", " Simulation\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " Many-body force\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " Strength\n", " \n", " \n", "
\n", "
\n", " Theta\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", " Min\n", " \n", " \n", "
\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", " Max\n", " \n", " \n", "
\n", "
\n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " Links force\n", "
\n", "
\n", "
\n", " \n", " \n", " \n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " Collision force\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " Radius\n", " \n", " \n", "
\n", "
\n", " Strength\n", " \n", " \n", "
\n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " x-positioning force\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " Strength\n", " \n", " \n", "
\n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " y-positioning force\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", " Strength\n", " \n", " \n", "
\n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " Centering force\n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", "
\n", "
\n", "
\n", "
\n", "\n", " \n", "\n" ], "text/plain": [ "" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "gv.d3(\n", " graph_gjgf,\n", " show_node_label=True, node_label_data_source='label',\n", " show_edge_label=True, edge_label_data_source='label', edge_curvature=0.4,\n", " graph_height=250, zoom_factor=1.5)" ] }, { "cell_type": "code", "execution_count": null, "id": "c2fae6df", "metadata": {}, "outputs": [], "source": [ "# Caution: vis.js is not well suited for drawing self loops and fails to display multi edges!\n", "# Please don't use it for graphs that include such edges, as the depictions can be misleading.\n", "gv.vis(\n", " graph_gjgf,\n", " show_node_label=True, node_label_data_source='label',\n", " show_edge_label=True, edge_label_data_source='label', edge_curvature=0.4,\n", " graph_height=250, zoom_factor=1.5)" ] }, { "cell_type": "code", "execution_count": null, "id": "f8091d68", "metadata": {}, "outputs": [], "source": [ "gv.three(\n", " graph_gjgf,\n", " show_node_label=True, node_label_data_source='label',\n", " show_edge_label=True, edge_label_data_source='label', edge_curvature=0.4,\n", " graph_height=250, zoom_factor=1.5)" ] }, { "cell_type": "markdown", "id": "0f7d1ef8", "metadata": {}, "source": [ "## Calculate a fixed layout with external libraries\n", "\n", "This example uses NetworkX to generate a graph and calculate a layout for it:\n", "\n", "- [barabasi_albert_graph](https://networkx.org/documentation/stable/reference/generated/networkx.generators.random_graphs.barabasi_albert_graph.html) is used to generate a random graph with Barabási–Albert preferential attachment\n", "- [spring_layout](https://networkx.org/documentation/stable/reference/generated/networkx.drawing.layout.spring_layout.html) is used to calculate x and y positions for each node" ] }, { "cell_type": "code", "execution_count": null, "id": "79698508", "metadata": {}, "outputs": [], "source": [ "# Generate graph\n", "graph = nx.barabasi_albert_graph(n=300, m=1)\n", "\n", "# Calculate layout\n", "pos = nx.drawing.layout.spring_layout(graph, scale=600)\n", "\n", "# Add coordinates as node annotations that are recognized by gravis\n", "for name, (x, y) in pos.items():\n", " node = graph.nodes[name]\n", " node['x'] = x\n", " node['y'] = y\n", "\n", "gv.d3(graph, zoom_factor=0.35, layout_algorithm_active=False)" ] }, { "cell_type": "markdown", "id": "d0aba4f6", "metadata": {}, "source": [ "## Plot a graph with all available arguments" ] }, { "cell_type": "code", "execution_count": null, "id": "19abebd1", "metadata": {}, "outputs": [], "source": [ "graph = nx.barabasi_albert_graph(n=80, m=1)\n", "\n", "gv.d3(\n", " data=graph,\n", " graph_height=200,\n", " details_height=100,\n", " show_details=True,\n", " show_details_toggle_button=True,\n", " show_menu=True,\n", " show_menu_toggle_button=True,\n", " show_node=True,\n", " node_size_factor=1.2,\n", " node_size_data_source='size',\n", " use_node_size_normalization=False,\n", " node_size_normalization_min=10.0,\n", " node_size_normalization_max=50.0,\n", " node_drag_fix=True,\n", " node_hover_neighborhood=True,\n", " node_hover_tooltip=True,\n", " show_node_image=True,\n", " node_image_size_factor=1.0,\n", " show_node_label=True,\n", " show_node_label_border=False,\n", " node_label_data_source='id',\n", " node_label_size_factor=0.8,\n", " node_label_rotation=45.0,\n", " node_label_font='Arial',\n", " show_edge=True,\n", " edge_size_factor=1.0,\n", " edge_size_data_source='size',\n", " use_edge_size_normalization=False,\n", " edge_size_normalization_min=0.2,\n", " edge_size_normalization_max=5.0,\n", " edge_curvature=0.0,\n", " edge_hover_tooltip=True,\n", " show_edge_label=True,\n", " show_edge_label_border=False,\n", " edge_label_data_source='id',\n", " edge_label_size_factor=1.0,\n", " edge_label_rotation=45.0,\n", " edge_label_font='Arial',\n", " zoom_factor=0.4,\n", " large_graph_threshold=500,\n", " layout_algorithm_active=True,\n", " \n", " # specific for d3\n", " use_many_body_force=True,\n", " many_body_force_strength=- 70.0,\n", " many_body_force_theta=0.9,\n", " use_many_body_force_min_distance=False,\n", " many_body_force_min_distance=10.0,\n", " use_many_body_force_max_distance=False,\n", " many_body_force_max_distance=1000.0,\n", " use_links_force=True,\n", " links_force_distance=50.0,\n", " links_force_strength=0.5,\n", " use_collision_force=True,\n", " collision_force_radius=30.0,\n", " collision_force_strength=0.9,\n", " use_x_positioning_force=False,\n", " x_positioning_force_strength=0.2,\n", " use_y_positioning_force=True,\n", " y_positioning_force_strength=0.5,\n", " use_centering_force=True,\n", ")" ] } ], "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 }