diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3f79a58
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,29 @@
+# binaries
+*.pyc
+.cache/
+.coverage
+
+# notebook checkpoints
+.ipynb_checkpoints/
+notebooks/.ipynb_checkpoints
+
+# test related
+.pytest_cache/
+pyvis/test.py
+
+# local files
+pyvis/full_graph.txt
+pyvis/source/stormofswords.csv
+
+# package details
+pyvis.egg-info/
+build/
+dist/
+pyvis/make.bat
+
+# vscode specific
+.vscode/
+venv
+
+# jetbrains specific files
+.idea
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..10e4d7a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,18 @@
+# Pull Requests
+
+Here are some guidelines for pull requests:
+
+* All work is submitted via Pull Requests.
+* Pull Requests can be submitted as soon as there is code worth discussing. The worst case is that the PR is closed.
+* Pull Requests should be made against master
+* Pull Requests should be tested, if feasible:
+ - bugfixes should include regression tests.
+ - new behavior should at least get minimal exercise.
+ - new features should include a screenshot
+* Don't make 'cleanup' pull requests just to change code style. We don't follow any style guide strictly, and we consider formatting changes unnecessary noise. If you're making functional changes, you can clean up the specific pieces of code you're working on.
+
+Pyvis wraps vis.js so JavaScript functionality issues should be directed at the main `vis.js` project.
+
+# Other contributions
+
+Outside of Pull Requests (PRs), we welcome additions/corrections/clarification to the existing documentation as contributions at least as valuable as code submissions.
diff --git a/LICENSE_BSD.txt b/LICENSE_BSD.txt
index 6e3050f..024edb5 100644
--- a/LICENSE_BSD.txt
+++ b/LICENSE_BSD.txt
@@ -11,7 +11,7 @@ are permitted provided that the following conditions are met:
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- - Neither the name of Thomas J Bradley nor the names of its contributors may
+ - Neither the name of West Health Institute nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
diff --git a/README.md b/README.md
index 19add10..7e79cb4 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,8 @@ python setup.py install
[ipython](https://ipython.org/ipython-doc/2/install/install.html)
+[jsonpickle](https://jsonpickle.github.io/)
+
## Quick Start
The most basic use case of a pyvis instance is to create a Network object and invoke methods:
@@ -37,4 +39,7 @@ g.add_node(0)
g.add_node(1)
g.add_edge(0, 1)
g.show("basic.html")
-```
\ No newline at end of file
+```
+
+## Interactive Notebook playground with examples
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/WestHealth/pyvis/master?filepath=notebooks%2Fexample.ipynb)
diff --git a/notebooks/dot.html b/notebooks/dot.html
new file mode 100644
index 0000000..d0b07ad
--- /dev/null
+++ b/notebooks/dot.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/notebooks/example.html b/notebooks/example.html
new file mode 100644
index 0000000..689d070
--- /dev/null
+++ b/notebooks/example.html
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/notebooks/example.ipynb b/notebooks/example.ipynb
new file mode 100644
index 0000000..e93d81b
--- /dev/null
+++ b/notebooks/example.ipynb
@@ -0,0 +1,483 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import sys\n",
+ "sys.path.append('../')\n",
+ "from pyvis.network import Network"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Basic Example"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "g = Network(notebook=True)\n",
+ "g.add_nodes(range(5))\n",
+ "g.add_edges([\n",
+ " (0, 2),\n",
+ " (0, 3),\n",
+ " (0, 4),\n",
+ " (1, 1),\n",
+ " (1, 3),\n",
+ " (1, 2)\n",
+ "])\n",
+ "g.show(\"example.html\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Dot File example"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "h = Network(notebook=True)\n",
+ "h.from_DOT(\"test.dot\")\n",
+ "\n",
+ "# All properties have to be enclosed by double quotes and \n",
+ "# there and there must be no comma at the end of a list.\n",
+ "# See https://visjs.github.io/vis-network/docs/network/ for all options\n",
+ "h.set_options(\"\"\"\n",
+ "var options = {\n",
+ " \"physics\": {\n",
+ " \"enabled\": true,\n",
+ " \"barnesHut\": {\n",
+ " \"theta\": 0.5,\n",
+ " \"gravitationalConstant\": -2000,\n",
+ " \"centralGravity\": 0.3,\n",
+ " \"springLength\": 200,\n",
+ " \"springConstant\": 0.04,\n",
+ " \"damping\": 0.09,\n",
+ " \"avoidOverlap\": 0\n",
+ " },\n",
+ " \"maxVelocity\": 50,\n",
+ " \"minVelocity\": 0.1,\n",
+ " \"solver\": \"barnesHut\",\n",
+ " \"stabilization\": {\n",
+ " \"enabled\": true,\n",
+ " \"iterations\": 1000,\n",
+ " \"updateInterval\": 100,\n",
+ " \"onlyDynamicEdges\": false,\n",
+ " \"fit\": true\n",
+ " }\n",
+ " }\n",
+ "}\n",
+ "\"\"\")\n",
+ "h.show(\"dot.html\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Basic NetworkX example"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import networkx as nx"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {
+ "scrolled": false
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "nxg = nx.random_tree(20)\n",
+ "g.from_nx(nxg)\n",
+ "g.show(\"example.html\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Disabling Physics interaction"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "g.toggle_physics(False)\n",
+ "g.show(\"example.html\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Explicit coordinates to layout nodes"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "g = Network(notebook=True)\n",
+ "g.add_nodes([1,2,3],\n",
+ " value=[10, 100, 400],\n",
+ " title=[\"I am node 1\", \"node 2 here\", \"and im node 3\"],\n",
+ " x=[21.4, 21.4, 21.4], y=[100.2, 223.54, 32.1],\n",
+ " label=[\"NODE 1\", \"NODE 2\", \"NODE 3\"],\n",
+ " color=[\"#00ff1e\", \"#162347\", \"#dd4b39\"])\n",
+ "g.show(\"example.html\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Full Game of Thrones example"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import pandas as pd\n",
+ "\n",
+ "got_net = Network(notebook=True, height=\"750px\", width=\"100%\", bgcolor=\"#222222\", font_color=\"white\")\n",
+ "\n",
+ "# set the physics layout of the network\n",
+ "got_net.barnes_hut()\n",
+ "got_data = pd.read_csv(\"https://www.macalester.edu/~abeverid/data/stormofswords.csv\")\n",
+ "\n",
+ "sources = got_data['Source']\n",
+ "targets = got_data['Target']\n",
+ "weights = got_data['Weight']\n",
+ "\n",
+ "edge_data = zip(sources, targets, weights)\n",
+ "\n",
+ "for e in edge_data:\n",
+ " src = e[0]\n",
+ " dst = e[1]\n",
+ " w = e[2]\n",
+ "\n",
+ " got_net.add_node(src, src, title=src)\n",
+ " got_net.add_node(dst, dst, title=dst)\n",
+ " got_net.add_edge(src, dst, value=w)\n",
+ "\n",
+ "neighbor_map = got_net.get_adj_list()\n",
+ "\n",
+ "# add neighbor data to node hover data\n",
+ "for node in got_net.nodes:\n",
+ " node[\"title\"] += \" Neighbors: \" + \" \".join(neighbor_map[node[\"id\"]])\n",
+ " node[\"value\"] = len(neighbor_map[node[\"id\"]]) # this value attrribute for the node affects node size\n",
+ "\n",
+ "got_net.show(\"example.html\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Experimenting with options UI\n",
+ "Scroll down underneath the graph to play around with the physics settings to acheive optimal layout and behavior. You can use the generate options button to display the JSON representation of the configuration."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "got_net.show_buttons(filter_=\"physics\")\n",
+ "got_net.show(\"example.html\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "got_net.set_options('''var options = {\n",
+ " \"physics\": {\n",
+ " \"barnesHut\": {\n",
+ " \"gravitationalConstant\": -80000,\n",
+ " \"springLength\": 250,\n",
+ " \"springConstant\": 0.001\n",
+ " },\n",
+ " \"maxVelocity\": 34,\n",
+ " \"minVelocity\": 0.75\n",
+ " }\n",
+ "}''')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import networkx as nx"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "nx_graph = nx.cycle_graph(10)\n",
+ "nx_graph.nodes[1]['title'] = 'Number 1'\n",
+ "nx_graph.nodes[1]['group'] = 1\n",
+ "nx_graph.nodes[3]['title'] = 'I belong to a different group!'\n",
+ "nx_graph.nodes[3]['group'] = 10\n",
+ "nx_graph.add_node(20, size=20, title='couple', group=2)\n",
+ "nx_graph.add_node(21, size=15, title='couple', group=2)\n",
+ "nx_graph.add_edge(20, 21, weight=5)\n",
+ "nx_graph.add_node(25, size=25, label='lonely', title='lonely node', group=3)\n",
+ "\n",
+ "nt = Network(notebook=True, height=\"750px\", width=\"100%\")\n",
+ "\n",
+ "nt.from_nx(nx_graph)\n",
+ "nt.show(\"nx.html\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "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.8.10"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/notebooks/test.dot b/notebooks/test.dot
new file mode 100644
index 0000000..c421f4f
--- /dev/null
+++ b/notebooks/test.dot
@@ -0,0 +1,9 @@
+digraph {
+ a [label=12, entity_id=12, entity_class="truck"];
+ b [label=7, entity_id=7,entity_class="bike"];
+ c [label=3, entity_id=3, entity_class="car"];
+ a -> b[label="solid edge"];
+ a -> b [label="dashed edge", style=dashed];
+ a -> c [label="dashed edge", style=dashed];
+ a -> c [label="dotted edge", style=dotted];
+}
\ No newline at end of file
diff --git a/pyvis/__init__.py b/pyvis/__init__.py
index 2f25379..011b8cd 100644
--- a/pyvis/__init__.py
+++ b/pyvis/__init__.py
@@ -1 +1,2 @@
-from . import network
\ No newline at end of file
+from . import network
+from ._version import __version__
\ No newline at end of file
diff --git a/pyvis/_version.py b/pyvis/_version.py
new file mode 100644
index 0000000..b4f711f
--- /dev/null
+++ b/pyvis/_version.py
@@ -0,0 +1 @@
+__version__ = '0.2.0' # bump version
diff --git a/pyvis/edge.py b/pyvis/edge.py
index 269e732..27d06fd 100644
--- a/pyvis/edge.py
+++ b/pyvis/edge.py
@@ -5,4 +5,5 @@ def __init__(self, source, dest, directed=False, **options):
self.options['from'] = source
self.options['to'] = dest
if directed:
- self.options["arrows"] = "from"
+ if 'arrows' not in self.options:
+ self.options["arrows"] = "to"
diff --git a/pyvis/network.py b/pyvis/network.py
index 29a35be..71d255c 100644
--- a/pyvis/network.py
+++ b/pyvis/network.py
@@ -1,13 +1,15 @@
from .node import Node
from .edge import Edge
from .options import Options, Configure
-from .utils import check_html
+from .utils import check_html, HREFParser
from jinja2 import Template
import webbrowser
from IPython.display import IFrame
+from IPython.core.display import HTML
from collections import defaultdict
import networkx as nx
import json
+import jsonpickle
import os
@@ -27,7 +29,9 @@ def __init__(self,
directed=False,
notebook=False,
bgcolor="#ffffff",
- font_color=False):
+ font_color=False,
+ layout=None,
+ heading=""):
"""
:param height: The height of the canvas
:param width: The width of the canvas
@@ -36,6 +40,7 @@ def __init__(self,
:param notebook: True if using jupyter notebook.
:param bgcolor: The background color of the canvas.
:font_color: The color of the node labels text
+ :layout: Use hierarchical layout if this is set
:type height: num or str
:type width: num or str
@@ -43,11 +48,13 @@ def __init__(self,
:type notebook: bool
:type bgcolor: str
:type font_color: str
+ :type layout: bool
"""
self.nodes = []
self.edges = []
self.height = height
self.width = width
+ self.heading = heading
self.html = ""
self.shape = "dot"
self.font_color = font_color
@@ -55,16 +62,17 @@ def __init__(self,
self.bgcolor = bgcolor
self.use_DOT = False
self.dot_lang = ""
- self.options = Options()
+ self.options = Options(layout)
self.widget = False
self.node_ids = []
+ self.node_map = {}
self.template = None
self.conf = False
self.path = os.path.dirname(__file__) + "/templates/template.html"
-
+
if notebook:
self.prep_notebook()
-
+
def __str__(self):
"""
override print to show readable graph data
@@ -75,7 +83,8 @@ def __str__(self):
"Nodes": self.node_ids,
"Edges": self.edges,
"Height": self.height,
- "Width": self.width
+ "Width": self.width,
+ "Heading": self.heading
},
indent=4
)
@@ -200,9 +209,11 @@ def add_node(self, n_id, label=None, shape="dot", **options):
else:
node_label = n_id
if n_id not in self.node_ids:
- n = Node(n_id, shape, label=node_label, font_color=self.font_color, **options)
+ n = Node(n_id, shape, label=node_label,
+ font_color=self.font_color, **options)
self.nodes.append(n.options)
self.node_ids.append(n_id)
+ self.node_map[n_id] = n.options
def add_nodes(self, nodes, **kwargs):
"""
@@ -228,7 +239,8 @@ def add_nodes(self, nodes, **kwargs):
:type nodes: list
"""
- valid_args = ["size", "value", "title", "x", "y", "label", "color"]
+ valid_args = ["size", "value", "title",
+ "x", "y", "label", "color", "shape"]
for k in kwargs:
assert k in valid_args, "invalid arg '" + k + "'"
@@ -245,8 +257,14 @@ def add_nodes(self, nodes, **kwargs):
nd[nodes[i]].update({k: v[i]})
for node in nodes:
- assert isinstance(node, int) or isinstance(node, str)
- self.add_node(node, **nd[node])
+ # check if node is `number-like`
+ try:
+ node = int(node)
+ self.add_node(node, **nd[node])
+ except:
+ # or node could be string
+ assert isinstance(node, str)
+ self.add_node(node, **nd[node])
def num_nodes(self):
"""
@@ -322,15 +340,17 @@ def add_edge(self, source, to, **options):
assert to in self.get_nodes(), \
"non existent node '" + str(to) + "'"
- for e in self.edges:
- frm = e['from']
- dest = e['to']
- if (
- (source == dest and to == frm) or
- (source == frm and to == dest)
- ):
- # edge already exists
- edge_exists = True
+ # we only check existing edge for undirected graphs
+ if not self.directed:
+ for e in self.edges:
+ frm = e['from']
+ dest = e['to']
+ if (
+ (source == dest and to == frm) or
+ (source == frm and to == dest)
+ ):
+ # edge already exists
+ edge_exists = True
if not edge_exists:
e = Edge(source, to, self.directed, **options)
@@ -366,9 +386,13 @@ def get_network_data(self):
Usage:
- >>> nodes, edges, height, width, options = net.get_network_data()
+ >>> nodes, edges, heading, height, width, options = net.get_network_data()
"""
- return (self.nodes, self.edges, self.height,
+ if isinstance(self.options, dict):
+ return (self.nodes, self.edges, self.heading, self.height,
+ self.width, json.dumps(self.options))
+ else:
+ return (self.nodes, self.edges, self.heading, self.height,
self.width, self.options.to_json())
def save_graph(self, name):
@@ -381,26 +405,26 @@ def save_graph(self, name):
check_html(name)
self.write_html(name)
- def write_html(self, name, notebook=False):
+ def generate_html(self, notebook=False):
"""
- This method gets the data structures supporting the nodes, edges,
- and options and updates the template to write the HTML holding
+ This method generates HTML from the data structures supporting the nodes, edges,
+ and options and updates the template to generate the HTML holding
the visualization.
- :type name_html: str
+ :type notebook: bool
+
+ Returns
+ :type out: str
"""
- check_html(name)
- # here, check if an href is present in the hover data
+
+ # here, check if a href is present in the hover data
use_link_template = False
for n in self.nodes:
title = n.get("title", None)
if title:
- if "href" in title:
- """
- this tells the template to override default hover
- mechanic, as the tooltip would move with the mouse
- cursor which made interacting with hover data useless.
- """
+ href_parser = HREFParser()
+ href_parser.feed(str(title))
+ if href_parser.is_valid():
use_link_template = True
break
if not notebook:
@@ -410,18 +434,43 @@ def write_html(self, name, notebook=False):
else:
template = self.template
- nodes, edges, height, width, options = self.get_network_data()
- self.html = template.render(height=height,
- width=width,
- nodes=nodes,
- edges=edges,
- options=options,
- use_DOT=self.use_DOT,
- dot_lang=self.dot_lang,
- widget=self.widget,
- bgcolor=self.bgcolor,
- conf=self.conf,
- tooltip_link=use_link_template)
+ nodes, edges, heading, height, width, options = self.get_network_data()
+
+ # check if physics is enabled
+ if isinstance(self.options, dict):
+ if 'physics' in self.options and 'enabled' in self.options['physics']:
+ physics_enabled = self.options['physics']['enabled']
+ else:
+ physics_enabled = True
+ else:
+ physics_enabled = self.options.physics.enabled
+
+ out = template.render(height=height,
+ width=width,
+ nodes=nodes,
+ edges=edges,
+ heading=heading,
+ options=options,
+ physics_enabled=physics_enabled,
+ use_DOT=self.use_DOT,
+ dot_lang=self.dot_lang,
+ widget=self.widget,
+ bgcolor=self.bgcolor,
+ conf=self.conf,
+ tooltip_link=use_link_template)
+
+ return out
+
+ def write_html(self, name, notebook=False):
+ """
+ This method gets the data structures supporting the nodes, edges,
+ and options and updates the template to write the HTML holding
+ the visualization.
+
+ :type name_html: str
+ """
+ check_html(name)
+ self.html = self.generate_html(notebook=notebook)
with open(name, "w+") as out:
out.write(self.html)
@@ -533,7 +582,8 @@ def neighbors(self, node):
assert(node in self.node_ids), "error: %s node not in network" % node
return self.get_adj_list()[node]
- def from_nx(self, nx_graph):
+ def from_nx(self, nx_graph, node_size_transf=(lambda x: x), edge_weight_transf=(lambda x: x),
+ default_node_size =10, default_edge_weight=1, show_edge_weights=True):
"""
This method takes an exisitng Networkx graph and translates
it to a PyVis graph format that can be accepted by the VisJs
@@ -541,22 +591,52 @@ def from_nx(self, nx_graph):
:param nx_graph: The Networkx graph object that is to be translated.
:type nx_graph: networkx.Graph instance
- >>> nx_graph = Networkx.cycle_graph()
+ :param node_size_transf: function to transform the node size for plotting
+ :type node_size_transf: func
+ :param edge_weight_transf: function to transform the edge weight for plotting
+ :type edge_weight_transf: func
+ :param default_node_size: default node size if not specified
+ :param default_edge_weight: default edge weight if not specified
+ >>> nx_graph = nx.cycle_graph(10)
+ >>> nx_graph.nodes[1]['title'] = 'Number 1'
+ >>> nx_graph.nodes[1]['group'] = 1
+ >>> nx_graph.nodes[3]['title'] = 'I belong to a different group!'
+ >>> nx_graph.nodes[3]['group'] = 10
+ >>> nx_graph.add_node(20, size=20, title='couple', group=2)
+ >>> nx_graph.add_node(21, size=15, title='couple', group=2)
+ >>> nx_graph.add_edge(20, 21, weight=5)
+ >>> nx_graph.add_node(25, size=25, label='lonely', title='lonely node', group=3)
>>> nt = Network("500px", "500px")
# populates the nodes and edges data structures
>>> nt.from_nx(nx_graph)
>>> nt.show("nx.html")
"""
assert(isinstance(nx_graph, nx.Graph))
- edges = nx_graph.edges(data=True)
- nodes = nx_graph.nodes()
+ edges=nx_graph.edges(data = True)
+ nodes=nx_graph.nodes(data = True)
+
if len(edges) > 0:
for e in edges:
- self.add_node(e[0], e[0], title=e[0])
- self.add_node(e[1], e[1], title=e[1])
- self.add_edge(e[0], e[1])
- else:
- self.add_nodes(nodes)
+ if 'size' not in nodes[e[0]].keys():
+ nodes[e[0]]['size']=default_node_size
+ nodes[e[0]]['size']=int(node_size_transf(nodes[e[0]]['size']))
+ if 'size' not in nodes[e[1]].keys():
+ nodes[e[1]]['size']=default_node_size
+ nodes[e[1]]['size']=int(node_size_transf(nodes[e[1]]['size']))
+ self.add_node(e[0], **nodes[e[0]])
+ self.add_node(e[1], **nodes[e[1]])
+
+ if 'weight' not in e[2].keys():
+ e[2]['weight']=default_edge_weight
+ e[2]['weight']=edge_weight_transf(e[2]['weight'])
+ if show_edge_weights:
+ e[2]["label"] = e[2]["weight"]
+ self.add_edge(e[0], e[1], **e[2])
+
+ for node in nx.isolates(nx_graph):
+ if 'size' not in nodes[node].keys():
+ nodes[node]['size']=default_node_size
+ self.add_node(node, **nodes[node])
def get_nodes(self):
"""
@@ -566,6 +646,16 @@ def get_nodes(self):
"""
return self.node_ids
+ def get_node(self, n_id):
+ """
+ Lookup node by ID and return it.
+
+ :param n_id: The ID given to the node.
+
+ :returns: dict containing node properties
+ """
+ return self.node_map[n_id]
+
def get_edges(self):
"""
This method returns an iterable list of edge objects
@@ -585,7 +675,7 @@ def barnes_hut(
):
"""
BarnesHut is a quadtree based gravity model. It is the fastest. default
- and recommended solver for non-heirarchical layouts.
+ and recommended solver for non-hierarchical layouts.
:param gravity: The more negative the gravity value is, the stronger the
repulsion is.
@@ -708,9 +798,8 @@ def force_atlas_2based(
"""
self.options.physics.use_force_atlas_2based(locals())
- def to_json(self):
- return json.dumps(self, default=lambda o: o.__dict__,
- sort_keys=True, indent=4)
+ def to_json(self, max_depth=1, **args):
+ return jsonpickle.encode(self, max_depth=max_depth, **args)
def set_edge_smooth(self, smooth_type):
"""
@@ -753,7 +842,7 @@ def toggle_hide_nodes_on_drag(self, status):
"""
self.options.interaction.hideNodesOnDrag = status
- def inherit_edge_colors_from(self, status):
+ def inherit_edge_colors(self, status):
"""
Edges take on the color of the node they are coming from.
@@ -768,7 +857,10 @@ def show_buttons(self, filter_=None):
network.
Usage:
- >>> g.toggle_buttons(filter_=['nodes', 'edges', 'physics'])
+ >>> g.show_buttons(filter_=['nodes', 'edges', 'physics'])
+
+ Or to show all options:
+ >>> g.show_buttons()
:param status: When set to True, the widgets will be shown.
Default is set to False.
@@ -788,11 +880,12 @@ def show_buttons(self, filter_=None):
def toggle_physics(self, status):
"""
- Displays or hides certain widgets to dynamically modify the
- network.
+ Toggles physics simulation
- :param status: When set to True, the widgets will be shown.
- Default is set to False.
+ :param status: When False, nodes are not part of the physics
+ simulation. They will not move except for from
+ manual dragging.
+ Default is set to True.
:type status: bool
"""
@@ -803,7 +896,7 @@ def toggle_drag_nodes(self, status):
Toggles the dragging of the nodes in the network.
:param status: When set to True, the nodes can be dragged around
- in the network. Default is set to False.
+ in the network. Default is set to True.
:type status: bool
"""
@@ -818,3 +911,15 @@ def toggle_stabilization(self, status):
:type status: bool
"""
self.options.physics.toggle_stabilization(status)
+
+ def set_options(self, options):
+ """
+ Overrides the default options object passed to the VisJS framework.
+ Delegates to the :meth:`options.Options.set` routine.
+
+ :param options: The string representation of the Javascript-like object
+ to be used to override default options.
+
+ :type options: str
+ """
+ self.options = self.options.set(options)
diff --git a/pyvis/options.py b/pyvis/options.py
index d58e218..0ba1526 100644
--- a/pyvis/options.py
+++ b/pyvis/options.py
@@ -1,15 +1,40 @@
from .physics import *
class EdgeOptions(object):
+ """
+ This is where the construction of the edges' options takes place.
+ So far, the edge smoothness can be switched through this object
+ as well as the edge color's inheritance.
+ """
def __init__(self):
self.smooth = self.Smooth()
self.color = self.Color()
def inherit_colors(self, status):
+ """
+ Whether or not to inherit colors from the source node.
+ If this is set to `from` then the edge will take the color
+ of the source node. If it is set to `to` then the color will
+ be that of the destination node.
+
+ .. note:: If set to `True` then the `from` behavior is adopted
+ and vice versa.
+ """
self.color.inherit = status
def toggle_smoothness(self, smooth_type):
+ """
+ Change smooth option for edges. When using dynamic, the edges will
+ have an invisible support node guiding the shape. This node is part
+ of the physics simulation,
+
+ :param smooth_type: Possible options are dynamic, continuous, discrete,
+ diagonalCross, straightCross, horizontal, vertical,
+ curvedCW, curvedCCW, cubicBezier
+
+ :type smooth_type: str
+ """
self.smooth.type = smooth_type
def __repr__(self):
@@ -30,13 +55,13 @@ def __repr__(self):
return str(self.__dict__)
def __init__(self):
- self.enabled = False
- self.type = "continuous"
+ self.enabled = True
+ self.type = "dynamic"
class Color(object):
"""
The color object contains the color information of the edge
- in every situation. When the edge only needs a sngle color value
+ in every situation. When the edge only needs a single color value
like 'rgb(120,32,14)', '#ffffff' or 'red' can be supplied instead
of an object.
"""
@@ -48,7 +73,10 @@ def __init__(self):
class Interaction(object):
-
+ """
+ Used for all user interaction with the network. Handles mouse
+ and touch events as well as the navigation buttons and the popups.
+ """
def __repr__(self):
return str(self.__dict__)
@@ -62,6 +90,10 @@ def __getitem__(self, item):
class Configure(object):
+ """
+ Handles the HTML part of the canvas and generates
+ an interactive option editor with filtering.
+ """
def __repr__(self):
return str(self.__dict__)
@@ -75,19 +107,124 @@ def __getitem__(self, item):
return self.__dict__[item]
+class Layout(object):
+ """
+ Acts as the camera that looks on the canvas.
+ Does the animation, zooming and focusing.
+ """
+
+ def __repr__(self):
+ return str(self.__dict__)
-class Options(object):
+ def __init__(self, randomSeed=None, improvedLayout=True):
+ if not randomSeed:
+ self.randomSeed = 0
+ else:
+ self.randomSeed = randomSeed
+ self.improvedLayout = improvedLayout
+ self.hierarchical = self.Hierarchical(enabled=True)
+
+ def set_separation(self, distance):
+ """
+ The distance between the different levels.
+ """
+ self.hierarchical.levelSeparation = distance
+
+ def set_tree_spacing(self, distance):
+ """
+ Distance between different trees (independent networks). This is
+ only for the initial layout. If you enable physics, the repulsion
+ model will denote the distance between the trees.
+ """
+ self.hierarchical.treeSpacing = distance
+
+ def set_edge_minimization(self, status):
+ """
+ Method for reducing whitespace. Can be used alone or together with
+ block shifting. Enabling block shifting will usually speed up the
+ layout process. Each node will try to move along its free axis to
+ reduce the total length of it's edges. This is mainly for the
+ initial layout. If you enable physics, they layout will be determined
+ by the physics. This will greatly speed up the stabilization time
+ """
+ self.hierarchical.edgeMinimization = status
+
+ class Hierarchical(object):
+
+ def __getitem__(self, item):
+ return self.__dict__[item]
+
+ def __init__(self,
+ enabled=False,
+ levelSeparation=150,
+ treeSpacing=200,
+ blockShifting=True,
+ edgeMinimization=True,
+ parentCentralization=True,
+ sortMethod='hubsize'):
+
+ self.enabled = enabled
+ self.levelSeparation = levelSeparation
+ self.treeSpacing = treeSpacing
+ self.blockShifting = blockShifting
+ self.edgeMinimization = edgeMinimization
+ self.parentCentralization = parentCentralization
+ self.sortMethod = sortMethod
+
+
+class Options(object):
+ """
+ Represents the global options of the network.
+ This object consists of indiviual sub-objects
+ that map to VisJS's modules of:
+ - configure
+ - layout
+ - interaction
+ - physics
+ - edges
+
+ The JSON representation of this object is directly passed
+ in to the VisJS framework.
+ In the future this can be expanded to completely mimic
+ the structure VisJS can expect.
+ """
def __repr__(self):
return str(self.__dict__)
- def __init__(self):
- # self.layout = Layout()
+ def __getitem__(self, item):
+ return self.__dict__[item]
+
+ def __init__(self, layout=None):
+ if layout:
+ self.layout = Layout()
self.interaction = Interaction()
self.configure = Configure()
self.physics = Physics()
self.edges = EdgeOptions()
+ def set(self, new_options):
+ """
+ This method should accept a JSON string and replace its internal
+ options structure with the given argument after parsing it.
+ In practice, this method should be called after using the browser
+ to experiment with different physics and layout options, using
+ the generated JSON options structure that is spit out from the
+ front end to serve as input to this method as a string.
+
+ :param new_options: The JSON like string of the options that will
+ override.
+
+ :type new_options: str
+ """
+
+ options = new_options.replace("\n", "").replace(" ", "")
+ first_bracket = options.find("{")
+ options = options[first_bracket:]
+ options = json.loads(options)
+ return options
+
+
def to_json(self):
return json.dumps(
self, default=lambda o: o.__dict__,
diff --git a/pyvis/physics.py b/pyvis/physics.py
index 9bfaf11..a5b5ff4 100644
--- a/pyvis/physics.py
+++ b/pyvis/physics.py
@@ -1,116 +1,116 @@
-import json
-
-
-class Physics(object):
-
- engine_chosen = False
-
- def __getitem__(self, item):
- return self.__dict__[item]
-
- def __repr__(self):
- return str(self.__dict__)
-
- class barnesHut(object):
- """
- BarnesHut is a quadtree based gravity model.
- This is the fastest, default and recommended.
- """
-
- def __init__(self, params):
- self.gravitationalConstant = params["gravity"]
- self.centralGravity = params["central_gravity"]
- self.springLength = params["spring_length"]
- self.springConstant = params["spring_strength"]
- self.damping = params["damping"]
- self.avoidOverlap = params["overlap"]
-
-
- class forceAtlas2Based(object):
- """
- Force Atlas 2 has been develoved by Jacomi et all (2014)
- for use with Gephi. The force Atlas based solver makes use
- of some of the equations provided by them and makes use of
- some of the barnesHut implementation in vis. The Main differences
- are the central gravity model, which is here distance independent,
- and repulsion being linear instead of quadratic. Finally, all node
- masses have a multiplier based on the amount of connected edges
- plus one.
- """
- def __init__(self, params):
- self.gravitationalConstant = params["gravity"]
- self.centralGravity = params["central_gravity"]
- self.springLength = params["spring_length"]
- self.springConstant = params["spring_strength"]
- self.damping = params["damping"]
- self.avoidOverlap = params["overlap"]
-
- class Repulsion(object):
- """
- The repulsion model assumes nodes have a simplified field
- around them. Its force lineraly decreases from 1
- (at 0.5*nodeDistace and smaller) to 0 (at 2*nodeDistance)
- """
- def __init__(self, params):
- self.nodeDistance = params['node_distance']
- self.centralGravity = params['central_gravity']
- self.springLength = params['spring_length']
- self.springConstant = params['spring_strength']
- self.damping = params['damping']
-
- class hierarchicalRepulsion(object):
- """
- This model is based on the repulsion solver but the levels
- are taken into accound and the forces
- are normalized.
- """
- def __init__(self, params):
- self.nodeDistance = params['node_distance']
- self.centralGravity = params['central_gravity']
- self.springLength = params['spring_length']
- self.springConstant = params['spring_strength']
- self.damping = params['damping']
-
- class Stabilization(object):
- """
- This makes the network stabilized on load using default settings.
- """
- def __getitem__(self, item):
- return self.__dict__[item]
-
- def __init__(self):
- self.enabled = True
- self.iterations = 1000
- self.updateInterval = 50
- self.onlyDynamicEdges = False
- self.fit = True
-
- def toggle_stabilization(self, status):
- self.enabled = status
-
- def __init__(self):
- self.enabled = True
- self.stabilization = self.Stabilization()
-
- def use_barnes_hut(self, params):
- self.barnesHut = self.barnesHut(params)
-
- def use_force_atlas_2based(self, params):
- self.forceAtlas2Based = self.forceAtlas2Based(params)
- self.solver = 'forceAtlas2Based'
-
- def use_repulsion(self, params):
- self.repulsion = self.Repulsion(params)
- self.solver = 'repulsion'
-
- def use_hrepulsion(self, params):
- self.hierarchicalRepulsion = self.hierarchicalRepulsion(params)
- self.solver = 'hierarchicalRepulsion'
-
- def toggle_stabilization(self, status):
- self.stabilization.toggle_stabilization(status)
-
- def to_json(self):
- return json.dumps(
- self, default=lambda o: o.__dict__,
- sort_keys=True, indent=4)
+import json
+
+
+class Physics(object):
+
+ engine_chosen = False
+
+ def __getitem__(self, item):
+ return self.__dict__[item]
+
+ def __repr__(self):
+ return str(self.__dict__)
+
+ class barnesHut(object):
+ """
+ BarnesHut is a quadtree based gravity model.
+ This is the fastest, default and recommended.
+ """
+
+ def __init__(self, params):
+ self.gravitationalConstant = params["gravity"]
+ self.centralGravity = params["central_gravity"]
+ self.springLength = params["spring_length"]
+ self.springConstant = params["spring_strength"]
+ self.damping = params["damping"]
+ self.avoidOverlap = params["overlap"]
+
+
+ class forceAtlas2Based(object):
+ """
+ Force Atlas 2 has been develoved by Jacomi et all (2014)
+ for use with Gephi. The force Atlas based solver makes use
+ of some of the equations provided by them and makes use of
+ some of the barnesHut implementation in vis. The Main differences
+ are the central gravity model, which is here distance independent,
+ and repulsion being linear instead of quadratic. Finally, all node
+ masses have a multiplier based on the amount of connected edges
+ plus one.
+ """
+ def __init__(self, params):
+ self.gravitationalConstant = params["gravity"]
+ self.centralGravity = params["central_gravity"]
+ self.springLength = params["spring_length"]
+ self.springConstant = params["spring_strength"]
+ self.damping = params["damping"]
+ self.avoidOverlap = params["overlap"]
+
+ class Repulsion(object):
+ """
+ The repulsion model assumes nodes have a simplified field
+ around them. Its force lineraly decreases from 1
+ (at 0.5*nodeDistace and smaller) to 0 (at 2*nodeDistance)
+ """
+ def __init__(self, params):
+ self.nodeDistance = params['node_distance']
+ self.centralGravity = params['central_gravity']
+ self.springLength = params['spring_length']
+ self.springConstant = params['spring_strength']
+ self.damping = params['damping']
+
+ class hierarchicalRepulsion(object):
+ """
+ This model is based on the repulsion solver but the levels
+ are taken into accound and the forces
+ are normalized.
+ """
+ def __init__(self, params):
+ self.nodeDistance = params['node_distance']
+ self.centralGravity = params['central_gravity']
+ self.springLength = params['spring_length']
+ self.springConstant = params['spring_strength']
+ self.damping = params['damping']
+
+ class Stabilization(object):
+ """
+ This makes the network stabilized on load using default settings.
+ """
+ def __getitem__(self, item):
+ return self.__dict__[item]
+
+ def __init__(self):
+ self.enabled = True
+ self.iterations = 1000
+ self.updateInterval = 50
+ self.onlyDynamicEdges = False
+ self.fit = True
+
+ def toggle_stabilization(self, status):
+ self.enabled = status
+
+ def __init__(self):
+ self.enabled = True
+ self.stabilization = self.Stabilization()
+
+ def use_barnes_hut(self, params):
+ self.barnesHut = self.barnesHut(params)
+
+ def use_force_atlas_2based(self, params):
+ self.forceAtlas2Based = self.forceAtlas2Based(params)
+ self.solver = 'forceAtlas2Based'
+
+ def use_repulsion(self, params):
+ self.repulsion = self.Repulsion(params)
+ self.solver = 'repulsion'
+
+ def use_hrepulsion(self, params):
+ self.hierarchicalRepulsion = self.hierarchicalRepulsion(params)
+ self.solver = 'hierarchicalRepulsion'
+
+ def toggle_stabilization(self, status):
+ self.stabilization.toggle_stabilization(status)
+
+ def to_json(self):
+ return json.dumps(
+ self, default=lambda o: o.__dict__,
+ sort_keys=True, indent=4)
diff --git a/pyvis/source/conf.py b/pyvis/source/conf.py
index f00eaf8..6cede2c 100644
--- a/pyvis/source/conf.py
+++ b/pyvis/source/conf.py
@@ -16,6 +16,9 @@
import os
import sphinx_rtd_theme
+# dynamically read the version
+exec(open('../_version.py').read())
+
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
@@ -63,7 +66,7 @@
# built documents.
#
# The short X.Y version.
-version = u'0.1'
+version = __version__
# The full version, including alpha/beta/rc tags.
release = u'0.1.3.1'
diff --git a/pyvis/source/documentation.rst b/pyvis/source/documentation.rst
index 760ce22..5bb6174 100644
--- a/pyvis/source/documentation.rst
+++ b/pyvis/source/documentation.rst
@@ -3,4 +3,7 @@ Documentation
=============
.. automodule:: pyvis.network
+ :members:
+
+.. automodule:: pyvis.options
:members:
\ No newline at end of file
diff --git a/pyvis/source/gameofthrones.html b/pyvis/source/gameofthrones.html
index 3a7399c..ae12fb2 100644
--- a/pyvis/source/gameofthrones.html
+++ b/pyvis/source/gameofthrones.html
@@ -8,7 +8,7 @@
+
+
+
+
+
+
+
+
+
+