Spaces:
Runtime error
Runtime error
update
Browse files- .pre-commit-config.yaml +1 -1
- pyvis +0 -1
- pyvis/.gitignore +26 -0
- pyvis/CONTRIBUTING.md +18 -0
- pyvis/MANIFEST.in +1 -0
- pyvis/README.md +45 -0
- pyvis/__init__.py +1 -0
- pyvis/notebooks/example.ipynb +483 -0
- pyvis/notebooks/test.dot +9 -0
- pyvis/pyproject.toml +3 -0
- pyvis/pyvis/Makefile +230 -0
- pyvis/pyvis/__init__.py +2 -0
- pyvis/pyvis/_version.py +1 -0
- pyvis/pyvis/edge.py +8 -0
- pyvis/pyvis/network.py +984 -0
- pyvis/pyvis/node.py +8 -0
- pyvis/pyvis/options.py +233 -0
- pyvis/pyvis/physics.py +117 -0
- pyvis/pyvis/source/conf.py +308 -0
- pyvis/pyvis/source/documentation.rst +9 -0
- pyvis/pyvis/source/index.rst +33 -0
- pyvis/pyvis/source/install.rst +20 -0
- pyvis/pyvis/source/introduction.rst +13 -0
- pyvis/pyvis/source/jup.png +0 -0
- pyvis/pyvis/source/license.rst +33 -0
- pyvis/pyvis/source/net.png +0 -0
- pyvis/pyvis/source/net2.png +0 -0
- pyvis/pyvis/source/net3.png +0 -0
- pyvis/pyvis/source/rednode.png +0 -0
- pyvis/pyvis/source/tutorial.rst +227 -0
- pyvis/pyvis/tests/__init__.py +0 -0
- pyvis/pyvis/tests/test_graph.py +335 -0
- pyvis/pyvis/tests/test_me.py +83 -0
- pyvis/pyvis/utils.py +12 -0
- pyvis/setup.py +26 -0
.pre-commit-config.yaml
CHANGED
@@ -21,7 +21,7 @@ repos:
|
|
21 |
language: system
|
22 |
entry: pipenv run flake8 py_code_analyzer
|
23 |
types: [python]
|
24 |
-
exclude: setup.py
|
25 |
|
26 |
- id: mypy
|
27 |
name: mypy
|
|
|
21 |
language: system
|
22 |
entry: pipenv run flake8 py_code_analyzer
|
23 |
types: [python]
|
24 |
+
exclude: setup.py|pyvis
|
25 |
|
26 |
- id: mypy
|
27 |
name: mypy
|
pyvis
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
Subproject commit e8249fb97d5797eaccce50d5c87bd45b76d9b5df
|
|
|
|
pyvis/.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# binaries
|
2 |
+
*.pyc
|
3 |
+
.cache/
|
4 |
+
.coverage
|
5 |
+
|
6 |
+
# notebook checkpoints
|
7 |
+
.ipynb_checkpoints/
|
8 |
+
notebooks/.ipynb_checkpoints
|
9 |
+
|
10 |
+
# test related
|
11 |
+
.pytest_cache/
|
12 |
+
pyvis/test.py
|
13 |
+
|
14 |
+
# local files
|
15 |
+
pyvis/full_graph.txt
|
16 |
+
pyvis/source/stormofswords.csv
|
17 |
+
|
18 |
+
# package details
|
19 |
+
pyvis.egg-info/
|
20 |
+
build/
|
21 |
+
dist/
|
22 |
+
pyvis/make.bat
|
23 |
+
|
24 |
+
# vscode specific
|
25 |
+
.vscode/
|
26 |
+
venv
|
pyvis/CONTRIBUTING.md
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Pull Requests
|
2 |
+
|
3 |
+
Here are some guidelines for pull requests:
|
4 |
+
|
5 |
+
* All work is submitted via Pull Requests.
|
6 |
+
* Pull Requests can be submitted as soon as there is code worth discussing. The worst case is that the PR is closed.
|
7 |
+
* Pull Requests should be made against master
|
8 |
+
* Pull Requests should be tested, if feasible:
|
9 |
+
- bugfixes should include regression tests.
|
10 |
+
- new behavior should at least get minimal exercise.
|
11 |
+
- new features should include a screenshot
|
12 |
+
* 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.
|
13 |
+
|
14 |
+
Pyvis wraps vis.js so JavaScript functionality issues should be directed at the main `vis.js` project.
|
15 |
+
|
16 |
+
# Other contributions
|
17 |
+
|
18 |
+
Outside of Pull Requests (PRs), we welcome additions/corrections/clarification to the existing documentation as contributions at least as valuable as code submissions.
|
pyvis/MANIFEST.in
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
recursive-include pyvis/templates *.html
|
pyvis/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
## Pyvis - a Python library for visualizing networks
|
2 |
+
|
3 |
+

|
4 |
+
|
5 |
+
## Description
|
6 |
+
Pyvis is built around [visjs](http://visjs.org/), a JavaScript visualization library.
|
7 |
+
|
8 |
+
## Documentation
|
9 |
+
Pyvis' full documentation can be found at http://pyvis.readthedocs.io/en/latest/
|
10 |
+
## Installation
|
11 |
+
You can install pyvis through pip:
|
12 |
+
|
13 |
+
```bash
|
14 |
+
pip install pyvis
|
15 |
+
```
|
16 |
+
Or if you have an archive of the project simply run the following from the top level directory:
|
17 |
+
|
18 |
+
```bash
|
19 |
+
python setup.py install
|
20 |
+
```
|
21 |
+
|
22 |
+
## Dependencies
|
23 |
+
[networkx](https://networkx.github.io/)
|
24 |
+
|
25 |
+
[jinja2](http://jinja.pocoo.org/)
|
26 |
+
|
27 |
+
[ipython](https://ipython.org/ipython-doc/2/install/install.html)
|
28 |
+
|
29 |
+
[jsonpickle](https://jsonpickle.github.io/)
|
30 |
+
|
31 |
+
## Quick Start
|
32 |
+
The most basic use case of a pyvis instance is to create a Network object and invoke methods:
|
33 |
+
|
34 |
+
```python
|
35 |
+
from pyvis.network import Network
|
36 |
+
|
37 |
+
g = Network()
|
38 |
+
g.add_node(0)
|
39 |
+
g.add_node(1)
|
40 |
+
g.add_edge(0, 1)
|
41 |
+
g.show("basic.html")
|
42 |
+
```
|
43 |
+
|
44 |
+
## Interactive Notebook playground with examples
|
45 |
+
[](https://mybinder.org/v2/gh/WestHealth/pyvis/master?filepath=notebooks%2Fexample.ipynb)
|
pyvis/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
from .pyvis.network import Network
|
pyvis/notebooks/example.ipynb
ADDED
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": 1,
|
6 |
+
"metadata": {},
|
7 |
+
"outputs": [],
|
8 |
+
"source": [
|
9 |
+
"import sys\n",
|
10 |
+
"sys.path.append('../')\n",
|
11 |
+
"from pyvis.network import Network"
|
12 |
+
]
|
13 |
+
},
|
14 |
+
{
|
15 |
+
"cell_type": "markdown",
|
16 |
+
"metadata": {},
|
17 |
+
"source": [
|
18 |
+
"### Basic Example"
|
19 |
+
]
|
20 |
+
},
|
21 |
+
{
|
22 |
+
"cell_type": "code",
|
23 |
+
"execution_count": 2,
|
24 |
+
"metadata": {},
|
25 |
+
"outputs": [
|
26 |
+
{
|
27 |
+
"data": {
|
28 |
+
"text/html": [
|
29 |
+
"\n",
|
30 |
+
" <iframe\n",
|
31 |
+
" width=\"500px\"\n",
|
32 |
+
" height=\"500px\"\n",
|
33 |
+
" src=\"example.html\"\n",
|
34 |
+
" frameborder=\"0\"\n",
|
35 |
+
" allowfullscreen\n",
|
36 |
+
" ></iframe>\n",
|
37 |
+
" "
|
38 |
+
],
|
39 |
+
"text/plain": [
|
40 |
+
"<IPython.lib.display.IFrame at 0x7fbe1d273d30>"
|
41 |
+
]
|
42 |
+
},
|
43 |
+
"execution_count": 2,
|
44 |
+
"metadata": {},
|
45 |
+
"output_type": "execute_result"
|
46 |
+
}
|
47 |
+
],
|
48 |
+
"source": [
|
49 |
+
"g = Network(notebook=True)\n",
|
50 |
+
"g.add_nodes(range(5))\n",
|
51 |
+
"g.add_edges([\n",
|
52 |
+
" (0, 2),\n",
|
53 |
+
" (0, 3),\n",
|
54 |
+
" (0, 4),\n",
|
55 |
+
" (1, 1),\n",
|
56 |
+
" (1, 3),\n",
|
57 |
+
" (1, 2)\n",
|
58 |
+
"])\n",
|
59 |
+
"g.show(\"example.html\")"
|
60 |
+
]
|
61 |
+
},
|
62 |
+
{
|
63 |
+
"cell_type": "markdown",
|
64 |
+
"metadata": {},
|
65 |
+
"source": [
|
66 |
+
"### Dot File example"
|
67 |
+
]
|
68 |
+
},
|
69 |
+
{
|
70 |
+
"cell_type": "code",
|
71 |
+
"execution_count": 9,
|
72 |
+
"metadata": {
|
73 |
+
"scrolled": true
|
74 |
+
},
|
75 |
+
"outputs": [
|
76 |
+
{
|
77 |
+
"data": {
|
78 |
+
"text/html": [
|
79 |
+
"\n",
|
80 |
+
" <iframe\n",
|
81 |
+
" width=\"500px\"\n",
|
82 |
+
" height=\"500px\"\n",
|
83 |
+
" src=\"dot.html\"\n",
|
84 |
+
" frameborder=\"0\"\n",
|
85 |
+
" allowfullscreen\n",
|
86 |
+
" ></iframe>\n",
|
87 |
+
" "
|
88 |
+
],
|
89 |
+
"text/plain": [
|
90 |
+
"<IPython.lib.display.IFrame at 0x7fbe3d2ead00>"
|
91 |
+
]
|
92 |
+
},
|
93 |
+
"execution_count": 9,
|
94 |
+
"metadata": {},
|
95 |
+
"output_type": "execute_result"
|
96 |
+
}
|
97 |
+
],
|
98 |
+
"source": [
|
99 |
+
"h = Network(notebook=True)\n",
|
100 |
+
"h.from_DOT(\"test.dot\")\n",
|
101 |
+
"\n",
|
102 |
+
"# All properties have to be enclosed by double quotes and \n",
|
103 |
+
"# there and there must be no comma at the end of a list.\n",
|
104 |
+
"# See https://visjs.github.io/vis-network/docs/network/ for all options\n",
|
105 |
+
"h.set_options(\"\"\"\n",
|
106 |
+
"var options = {\n",
|
107 |
+
" \"physics\": {\n",
|
108 |
+
" \"enabled\": true,\n",
|
109 |
+
" \"barnesHut\": {\n",
|
110 |
+
" \"theta\": 0.5,\n",
|
111 |
+
" \"gravitationalConstant\": -2000,\n",
|
112 |
+
" \"centralGravity\": 0.3,\n",
|
113 |
+
" \"springLength\": 200,\n",
|
114 |
+
" \"springConstant\": 0.04,\n",
|
115 |
+
" \"damping\": 0.09,\n",
|
116 |
+
" \"avoidOverlap\": 0\n",
|
117 |
+
" },\n",
|
118 |
+
" \"maxVelocity\": 50,\n",
|
119 |
+
" \"minVelocity\": 0.1,\n",
|
120 |
+
" \"solver\": \"barnesHut\",\n",
|
121 |
+
" \"stabilization\": {\n",
|
122 |
+
" \"enabled\": true,\n",
|
123 |
+
" \"iterations\": 1000,\n",
|
124 |
+
" \"updateInterval\": 100,\n",
|
125 |
+
" \"onlyDynamicEdges\": false,\n",
|
126 |
+
" \"fit\": true\n",
|
127 |
+
" }\n",
|
128 |
+
" }\n",
|
129 |
+
"}\n",
|
130 |
+
"\"\"\")\n",
|
131 |
+
"h.show(\"dot.html\")"
|
132 |
+
]
|
133 |
+
},
|
134 |
+
{
|
135 |
+
"cell_type": "markdown",
|
136 |
+
"metadata": {},
|
137 |
+
"source": [
|
138 |
+
"### Basic NetworkX example"
|
139 |
+
]
|
140 |
+
},
|
141 |
+
{
|
142 |
+
"cell_type": "code",
|
143 |
+
"execution_count": 10,
|
144 |
+
"metadata": {},
|
145 |
+
"outputs": [],
|
146 |
+
"source": [
|
147 |
+
"import networkx as nx"
|
148 |
+
]
|
149 |
+
},
|
150 |
+
{
|
151 |
+
"cell_type": "code",
|
152 |
+
"execution_count": 11,
|
153 |
+
"metadata": {
|
154 |
+
"scrolled": false
|
155 |
+
},
|
156 |
+
"outputs": [
|
157 |
+
{
|
158 |
+
"data": {
|
159 |
+
"text/html": [
|
160 |
+
"\n",
|
161 |
+
" <iframe\n",
|
162 |
+
" width=\"500px\"\n",
|
163 |
+
" height=\"500px\"\n",
|
164 |
+
" src=\"example.html\"\n",
|
165 |
+
" frameborder=\"0\"\n",
|
166 |
+
" allowfullscreen\n",
|
167 |
+
" ></iframe>\n",
|
168 |
+
" "
|
169 |
+
],
|
170 |
+
"text/plain": [
|
171 |
+
"<IPython.lib.display.IFrame at 0x7f3ef910af40>"
|
172 |
+
]
|
173 |
+
},
|
174 |
+
"execution_count": 11,
|
175 |
+
"metadata": {},
|
176 |
+
"output_type": "execute_result"
|
177 |
+
}
|
178 |
+
],
|
179 |
+
"source": [
|
180 |
+
"nxg = nx.random_tree(20)\n",
|
181 |
+
"g.from_nx(nxg)\n",
|
182 |
+
"g.show(\"example.html\")"
|
183 |
+
]
|
184 |
+
},
|
185 |
+
{
|
186 |
+
"cell_type": "markdown",
|
187 |
+
"metadata": {},
|
188 |
+
"source": [
|
189 |
+
"### Disabling Physics interaction"
|
190 |
+
]
|
191 |
+
},
|
192 |
+
{
|
193 |
+
"cell_type": "code",
|
194 |
+
"execution_count": 6,
|
195 |
+
"metadata": {},
|
196 |
+
"outputs": [
|
197 |
+
{
|
198 |
+
"data": {
|
199 |
+
"text/html": [
|
200 |
+
"\n",
|
201 |
+
" <iframe\n",
|
202 |
+
" width=\"500px\"\n",
|
203 |
+
" height=\"500px\"\n",
|
204 |
+
" src=\"example.html\"\n",
|
205 |
+
" frameborder=\"0\"\n",
|
206 |
+
" allowfullscreen\n",
|
207 |
+
" \n",
|
208 |
+
" ></iframe>\n",
|
209 |
+
" "
|
210 |
+
],
|
211 |
+
"text/plain": [
|
212 |
+
"<IPython.lib.display.IFrame at 0x7f882040c6d0>"
|
213 |
+
]
|
214 |
+
},
|
215 |
+
"execution_count": 6,
|
216 |
+
"metadata": {},
|
217 |
+
"output_type": "execute_result"
|
218 |
+
}
|
219 |
+
],
|
220 |
+
"source": [
|
221 |
+
"g.toggle_physics(False)\n",
|
222 |
+
"g.show(\"example.html\")"
|
223 |
+
]
|
224 |
+
},
|
225 |
+
{
|
226 |
+
"cell_type": "markdown",
|
227 |
+
"metadata": {},
|
228 |
+
"source": [
|
229 |
+
"### Explicit coordinates to layout nodes"
|
230 |
+
]
|
231 |
+
},
|
232 |
+
{
|
233 |
+
"cell_type": "code",
|
234 |
+
"execution_count": 7,
|
235 |
+
"metadata": {},
|
236 |
+
"outputs": [
|
237 |
+
{
|
238 |
+
"data": {
|
239 |
+
"text/html": [
|
240 |
+
"\n",
|
241 |
+
" <iframe\n",
|
242 |
+
" width=\"500px\"\n",
|
243 |
+
" height=\"500px\"\n",
|
244 |
+
" src=\"example.html\"\n",
|
245 |
+
" frameborder=\"0\"\n",
|
246 |
+
" allowfullscreen\n",
|
247 |
+
" \n",
|
248 |
+
" ></iframe>\n",
|
249 |
+
" "
|
250 |
+
],
|
251 |
+
"text/plain": [
|
252 |
+
"<IPython.lib.display.IFrame at 0x7f87dc679b50>"
|
253 |
+
]
|
254 |
+
},
|
255 |
+
"execution_count": 7,
|
256 |
+
"metadata": {},
|
257 |
+
"output_type": "execute_result"
|
258 |
+
}
|
259 |
+
],
|
260 |
+
"source": [
|
261 |
+
"g = Network(notebook=True)\n",
|
262 |
+
"g.add_nodes([1,2,3],\n",
|
263 |
+
" value=[10, 100, 400],\n",
|
264 |
+
" title=[\"I am node 1\", \"node 2 here\", \"and im node 3\"],\n",
|
265 |
+
" x=[21.4, 21.4, 21.4], y=[100.2, 223.54, 32.1],\n",
|
266 |
+
" label=[\"NODE 1\", \"NODE 2\", \"NODE 3\"],\n",
|
267 |
+
" color=[\"#00ff1e\", \"#162347\", \"#dd4b39\"])\n",
|
268 |
+
"g.show(\"example.html\")"
|
269 |
+
]
|
270 |
+
},
|
271 |
+
{
|
272 |
+
"cell_type": "markdown",
|
273 |
+
"metadata": {},
|
274 |
+
"source": [
|
275 |
+
"### Full Game of Thrones example"
|
276 |
+
]
|
277 |
+
},
|
278 |
+
{
|
279 |
+
"cell_type": "code",
|
280 |
+
"execution_count": 8,
|
281 |
+
"metadata": {},
|
282 |
+
"outputs": [
|
283 |
+
{
|
284 |
+
"data": {
|
285 |
+
"text/html": [
|
286 |
+
"\n",
|
287 |
+
" <iframe\n",
|
288 |
+
" width=\"100%\"\n",
|
289 |
+
" height=\"750px\"\n",
|
290 |
+
" src=\"example.html\"\n",
|
291 |
+
" frameborder=\"0\"\n",
|
292 |
+
" allowfullscreen\n",
|
293 |
+
" \n",
|
294 |
+
" ></iframe>\n",
|
295 |
+
" "
|
296 |
+
],
|
297 |
+
"text/plain": [
|
298 |
+
"<IPython.lib.display.IFrame at 0x7f87da76e430>"
|
299 |
+
]
|
300 |
+
},
|
301 |
+
"execution_count": 8,
|
302 |
+
"metadata": {},
|
303 |
+
"output_type": "execute_result"
|
304 |
+
}
|
305 |
+
],
|
306 |
+
"source": [
|
307 |
+
"import pandas as pd\n",
|
308 |
+
"\n",
|
309 |
+
"got_net = Network(notebook=True, height=\"750px\", width=\"100%\", bgcolor=\"#222222\", font_color=\"white\")\n",
|
310 |
+
"\n",
|
311 |
+
"# set the physics layout of the network\n",
|
312 |
+
"got_net.barnes_hut()\n",
|
313 |
+
"got_data = pd.read_csv(\"https://www.macalester.edu/~abeverid/data/stormofswords.csv\")\n",
|
314 |
+
"\n",
|
315 |
+
"sources = got_data['Source']\n",
|
316 |
+
"targets = got_data['Target']\n",
|
317 |
+
"weights = got_data['Weight']\n",
|
318 |
+
"\n",
|
319 |
+
"edge_data = zip(sources, targets, weights)\n",
|
320 |
+
"\n",
|
321 |
+
"for e in edge_data:\n",
|
322 |
+
" src = e[0]\n",
|
323 |
+
" dst = e[1]\n",
|
324 |
+
" w = e[2]\n",
|
325 |
+
"\n",
|
326 |
+
" got_net.add_node(src, src, title=src)\n",
|
327 |
+
" got_net.add_node(dst, dst, title=dst)\n",
|
328 |
+
" got_net.add_edge(src, dst, value=w)\n",
|
329 |
+
"\n",
|
330 |
+
"neighbor_map = got_net.get_adj_list()\n",
|
331 |
+
"\n",
|
332 |
+
"# add neighbor data to node hover data\n",
|
333 |
+
"for node in got_net.nodes:\n",
|
334 |
+
" node[\"title\"] += \" Neighbors:<br>\" + \"<br>\".join(neighbor_map[node[\"id\"]])\n",
|
335 |
+
" node[\"value\"] = len(neighbor_map[node[\"id\"]]) # this value attrribute for the node affects node size\n",
|
336 |
+
"\n",
|
337 |
+
"got_net.show(\"example.html\")"
|
338 |
+
]
|
339 |
+
},
|
340 |
+
{
|
341 |
+
"cell_type": "markdown",
|
342 |
+
"metadata": {},
|
343 |
+
"source": [
|
344 |
+
"### Experimenting with options UI\n",
|
345 |
+
"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."
|
346 |
+
]
|
347 |
+
},
|
348 |
+
{
|
349 |
+
"cell_type": "code",
|
350 |
+
"execution_count": 9,
|
351 |
+
"metadata": {},
|
352 |
+
"outputs": [
|
353 |
+
{
|
354 |
+
"data": {
|
355 |
+
"text/html": [
|
356 |
+
"\n",
|
357 |
+
" <iframe\n",
|
358 |
+
" width=\"100%\"\n",
|
359 |
+
" height=\"750px\"\n",
|
360 |
+
" src=\"example.html\"\n",
|
361 |
+
" frameborder=\"0\"\n",
|
362 |
+
" allowfullscreen\n",
|
363 |
+
" \n",
|
364 |
+
" ></iframe>\n",
|
365 |
+
" "
|
366 |
+
],
|
367 |
+
"text/plain": [
|
368 |
+
"<IPython.lib.display.IFrame at 0x7f87da76ee50>"
|
369 |
+
]
|
370 |
+
},
|
371 |
+
"execution_count": 9,
|
372 |
+
"metadata": {},
|
373 |
+
"output_type": "execute_result"
|
374 |
+
}
|
375 |
+
],
|
376 |
+
"source": [
|
377 |
+
"got_net.show_buttons(filter_=\"physics\")\n",
|
378 |
+
"got_net.show(\"example.html\")"
|
379 |
+
]
|
380 |
+
},
|
381 |
+
{
|
382 |
+
"cell_type": "code",
|
383 |
+
"execution_count": 13,
|
384 |
+
"metadata": {},
|
385 |
+
"outputs": [],
|
386 |
+
"source": [
|
387 |
+
"got_net.set_options('''var options = {\n",
|
388 |
+
" \"physics\": {\n",
|
389 |
+
" \"barnesHut\": {\n",
|
390 |
+
" \"gravitationalConstant\": -80000,\n",
|
391 |
+
" \"springLength\": 250,\n",
|
392 |
+
" \"springConstant\": 0.001\n",
|
393 |
+
" },\n",
|
394 |
+
" \"maxVelocity\": 34,\n",
|
395 |
+
" \"minVelocity\": 0.75\n",
|
396 |
+
" }\n",
|
397 |
+
"}''')"
|
398 |
+
]
|
399 |
+
},
|
400 |
+
{
|
401 |
+
"cell_type": "code",
|
402 |
+
"execution_count": 10,
|
403 |
+
"metadata": {},
|
404 |
+
"outputs": [],
|
405 |
+
"source": [
|
406 |
+
"import networkx as nx"
|
407 |
+
]
|
408 |
+
},
|
409 |
+
{
|
410 |
+
"cell_type": "code",
|
411 |
+
"execution_count": 11,
|
412 |
+
"metadata": {},
|
413 |
+
"outputs": [
|
414 |
+
{
|
415 |
+
"data": {
|
416 |
+
"text/html": [
|
417 |
+
"\n",
|
418 |
+
" <iframe\n",
|
419 |
+
" width=\"100%\"\n",
|
420 |
+
" height=\"750px\"\n",
|
421 |
+
" src=\"nx.html\"\n",
|
422 |
+
" frameborder=\"0\"\n",
|
423 |
+
" allowfullscreen\n",
|
424 |
+
" \n",
|
425 |
+
" ></iframe>\n",
|
426 |
+
" "
|
427 |
+
],
|
428 |
+
"text/plain": [
|
429 |
+
"<IPython.lib.display.IFrame at 0x7f88443e75b0>"
|
430 |
+
]
|
431 |
+
},
|
432 |
+
"execution_count": 11,
|
433 |
+
"metadata": {},
|
434 |
+
"output_type": "execute_result"
|
435 |
+
}
|
436 |
+
],
|
437 |
+
"source": [
|
438 |
+
"nx_graph = nx.cycle_graph(10)\n",
|
439 |
+
"nx_graph.nodes[1]['title'] = 'Number 1'\n",
|
440 |
+
"nx_graph.nodes[1]['group'] = 1\n",
|
441 |
+
"nx_graph.nodes[3]['title'] = 'I belong to a different group!'\n",
|
442 |
+
"nx_graph.nodes[3]['group'] = 10\n",
|
443 |
+
"nx_graph.add_node(20, size=20, title='couple', group=2)\n",
|
444 |
+
"nx_graph.add_node(21, size=15, title='couple', group=2)\n",
|
445 |
+
"nx_graph.add_edge(20, 21, weight=5)\n",
|
446 |
+
"nx_graph.add_node(25, size=25, label='lonely', title='lonely node', group=3)\n",
|
447 |
+
"\n",
|
448 |
+
"nt = Network(notebook=True, height=\"750px\", width=\"100%\")\n",
|
449 |
+
"\n",
|
450 |
+
"nt.from_nx(nx_graph)\n",
|
451 |
+
"nt.show(\"nx.html\")"
|
452 |
+
]
|
453 |
+
},
|
454 |
+
{
|
455 |
+
"cell_type": "code",
|
456 |
+
"execution_count": null,
|
457 |
+
"metadata": {},
|
458 |
+
"outputs": [],
|
459 |
+
"source": []
|
460 |
+
}
|
461 |
+
],
|
462 |
+
"metadata": {
|
463 |
+
"kernelspec": {
|
464 |
+
"display_name": "Python 3",
|
465 |
+
"language": "python",
|
466 |
+
"name": "python3"
|
467 |
+
},
|
468 |
+
"language_info": {
|
469 |
+
"codemirror_mode": {
|
470 |
+
"name": "ipython",
|
471 |
+
"version": 3
|
472 |
+
},
|
473 |
+
"file_extension": ".py",
|
474 |
+
"mimetype": "text/x-python",
|
475 |
+
"name": "python",
|
476 |
+
"nbconvert_exporter": "python",
|
477 |
+
"pygments_lexer": "ipython3",
|
478 |
+
"version": "3.8.10"
|
479 |
+
}
|
480 |
+
},
|
481 |
+
"nbformat": 4,
|
482 |
+
"nbformat_minor": 2
|
483 |
+
}
|
pyvis/notebooks/test.dot
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
digraph {
|
2 |
+
a [label=12, entity_id=12, entity_class="truck"];
|
3 |
+
b [label=7, entity_id=7,entity_class="bike"];
|
4 |
+
c [label=3, entity_id=3, entity_class="car"];
|
5 |
+
a -> b[label="solid edge"];
|
6 |
+
a -> b [label="dashed edge", style=dashed];
|
7 |
+
a -> c [label="dashed edge", style=dashed];
|
8 |
+
a -> c [label="dotted edge", style=dotted];
|
9 |
+
}
|
pyvis/pyproject.toml
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
[build-system]
|
2 |
+
requires = ["setuptools", "wheel"]
|
3 |
+
build-backend = "setuptools.build_meta:__legacy__"
|
pyvis/pyvis/Makefile
ADDED
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Makefile for Sphinx documentation
|
2 |
+
#
|
3 |
+
|
4 |
+
# You can set these variables from the command line.
|
5 |
+
SPHINXOPTS =
|
6 |
+
SPHINXBUILD = sphinx-build
|
7 |
+
PAPER =
|
8 |
+
BUILDDIR = build
|
9 |
+
|
10 |
+
# User-friendly check for sphinx-build
|
11 |
+
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
12 |
+
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/)
|
13 |
+
endif
|
14 |
+
|
15 |
+
# Internal variables.
|
16 |
+
PAPEROPT_a4 = -D latex_paper_size=a4
|
17 |
+
PAPEROPT_letter = -D latex_paper_size=letter
|
18 |
+
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
19 |
+
# the i18n builder cannot share the environment and doctrees with the others
|
20 |
+
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
21 |
+
|
22 |
+
.PHONY: help
|
23 |
+
help:
|
24 |
+
@echo "Please use \`make <target>' where <target> is one of"
|
25 |
+
@echo " html to make standalone HTML files"
|
26 |
+
@echo " dirhtml to make HTML files named index.html in directories"
|
27 |
+
@echo " singlehtml to make a single large HTML file"
|
28 |
+
@echo " pickle to make pickle files"
|
29 |
+
@echo " json to make JSON files"
|
30 |
+
@echo " htmlhelp to make HTML files and a HTML help project"
|
31 |
+
@echo " qthelp to make HTML files and a qthelp project"
|
32 |
+
@echo " applehelp to make an Apple Help Book"
|
33 |
+
@echo " devhelp to make HTML files and a Devhelp project"
|
34 |
+
@echo " epub to make an epub"
|
35 |
+
@echo " epub3 to make an epub3"
|
36 |
+
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
37 |
+
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
38 |
+
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
39 |
+
@echo " text to make text files"
|
40 |
+
@echo " man to make manual pages"
|
41 |
+
@echo " texinfo to make Texinfo files"
|
42 |
+
@echo " info to make Texinfo files and run them through makeinfo"
|
43 |
+
@echo " gettext to make PO message catalogs"
|
44 |
+
@echo " changes to make an overview of all changed/added/deprecated items"
|
45 |
+
@echo " xml to make Docutils-native XML files"
|
46 |
+
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
47 |
+
@echo " linkcheck to check all external links for integrity"
|
48 |
+
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
49 |
+
@echo " coverage to run coverage check of the documentation (if enabled)"
|
50 |
+
@echo " dummy to check syntax errors of document sources"
|
51 |
+
|
52 |
+
.PHONY: clean
|
53 |
+
clean:
|
54 |
+
rm -rf $(BUILDDIR)/*
|
55 |
+
|
56 |
+
.PHONY: html
|
57 |
+
html:
|
58 |
+
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
59 |
+
@echo
|
60 |
+
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
61 |
+
|
62 |
+
.PHONY: dirhtml
|
63 |
+
dirhtml:
|
64 |
+
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
65 |
+
@echo
|
66 |
+
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
67 |
+
|
68 |
+
.PHONY: singlehtml
|
69 |
+
singlehtml:
|
70 |
+
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
71 |
+
@echo
|
72 |
+
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
73 |
+
|
74 |
+
.PHONY: pickle
|
75 |
+
pickle:
|
76 |
+
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
77 |
+
@echo
|
78 |
+
@echo "Build finished; now you can process the pickle files."
|
79 |
+
|
80 |
+
.PHONY: json
|
81 |
+
json:
|
82 |
+
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
83 |
+
@echo
|
84 |
+
@echo "Build finished; now you can process the JSON files."
|
85 |
+
|
86 |
+
.PHONY: htmlhelp
|
87 |
+
htmlhelp:
|
88 |
+
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
89 |
+
@echo
|
90 |
+
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
91 |
+
".hhp project file in $(BUILDDIR)/htmlhelp."
|
92 |
+
|
93 |
+
.PHONY: qthelp
|
94 |
+
qthelp:
|
95 |
+
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
96 |
+
@echo
|
97 |
+
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
98 |
+
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
99 |
+
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyvis.qhcp"
|
100 |
+
@echo "To view the help file:"
|
101 |
+
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyvis.qhc"
|
102 |
+
|
103 |
+
.PHONY: applehelp
|
104 |
+
applehelp:
|
105 |
+
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
106 |
+
@echo
|
107 |
+
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
108 |
+
@echo "N.B. You won't be able to view it unless you put it in" \
|
109 |
+
"~/Library/Documentation/Help or install it in your application" \
|
110 |
+
"bundle."
|
111 |
+
|
112 |
+
.PHONY: devhelp
|
113 |
+
devhelp:
|
114 |
+
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
115 |
+
@echo
|
116 |
+
@echo "Build finished."
|
117 |
+
@echo "To view the help file:"
|
118 |
+
@echo "# mkdir -p $$HOME/.local/share/devhelp/pyvis"
|
119 |
+
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyvis"
|
120 |
+
@echo "# devhelp"
|
121 |
+
|
122 |
+
.PHONY: epub
|
123 |
+
epub:
|
124 |
+
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
125 |
+
@echo
|
126 |
+
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
127 |
+
|
128 |
+
.PHONY: epub3
|
129 |
+
epub3:
|
130 |
+
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
|
131 |
+
@echo
|
132 |
+
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
|
133 |
+
|
134 |
+
.PHONY: latex
|
135 |
+
latex:
|
136 |
+
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
137 |
+
@echo
|
138 |
+
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
139 |
+
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
140 |
+
"(use \`make latexpdf' here to do that automatically)."
|
141 |
+
|
142 |
+
.PHONY: latexpdf
|
143 |
+
latexpdf:
|
144 |
+
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
145 |
+
@echo "Running LaTeX files through pdflatex..."
|
146 |
+
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
147 |
+
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
148 |
+
|
149 |
+
.PHONY: latexpdfja
|
150 |
+
latexpdfja:
|
151 |
+
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
152 |
+
@echo "Running LaTeX files through platex and dvipdfmx..."
|
153 |
+
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
154 |
+
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
155 |
+
|
156 |
+
.PHONY: text
|
157 |
+
text:
|
158 |
+
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
159 |
+
@echo
|
160 |
+
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
161 |
+
|
162 |
+
.PHONY: man
|
163 |
+
man:
|
164 |
+
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
165 |
+
@echo
|
166 |
+
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
167 |
+
|
168 |
+
.PHONY: texinfo
|
169 |
+
texinfo:
|
170 |
+
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
171 |
+
@echo
|
172 |
+
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
173 |
+
@echo "Run \`make' in that directory to run these through makeinfo" \
|
174 |
+
"(use \`make info' here to do that automatically)."
|
175 |
+
|
176 |
+
.PHONY: info
|
177 |
+
info:
|
178 |
+
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
179 |
+
@echo "Running Texinfo files through makeinfo..."
|
180 |
+
make -C $(BUILDDIR)/texinfo info
|
181 |
+
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
182 |
+
|
183 |
+
.PHONY: gettext
|
184 |
+
gettext:
|
185 |
+
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
186 |
+
@echo
|
187 |
+
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
188 |
+
|
189 |
+
.PHONY: changes
|
190 |
+
changes:
|
191 |
+
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
192 |
+
@echo
|
193 |
+
@echo "The overview file is in $(BUILDDIR)/changes."
|
194 |
+
|
195 |
+
.PHONY: linkcheck
|
196 |
+
linkcheck:
|
197 |
+
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
198 |
+
@echo
|
199 |
+
@echo "Link check complete; look for any errors in the above output " \
|
200 |
+
"or in $(BUILDDIR)/linkcheck/output.txt."
|
201 |
+
|
202 |
+
.PHONY: doctest
|
203 |
+
doctest:
|
204 |
+
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
205 |
+
@echo "Testing of doctests in the sources finished, look at the " \
|
206 |
+
"results in $(BUILDDIR)/doctest/output.txt."
|
207 |
+
|
208 |
+
.PHONY: coverage
|
209 |
+
coverage:
|
210 |
+
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
211 |
+
@echo "Testing of coverage in the sources finished, look at the " \
|
212 |
+
"results in $(BUILDDIR)/coverage/python.txt."
|
213 |
+
|
214 |
+
.PHONY: xml
|
215 |
+
xml:
|
216 |
+
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
217 |
+
@echo
|
218 |
+
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
219 |
+
|
220 |
+
.PHONY: pseudoxml
|
221 |
+
pseudoxml:
|
222 |
+
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
223 |
+
@echo
|
224 |
+
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
225 |
+
|
226 |
+
.PHONY: dummy
|
227 |
+
dummy:
|
228 |
+
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
|
229 |
+
@echo
|
230 |
+
@echo "Build finished. Dummy builder generates no files."
|
pyvis/pyvis/__init__.py
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
from . import network
|
2 |
+
from ._version import __version__
|
pyvis/pyvis/_version.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
__version__ = "0.2.0" # bump version
|
pyvis/pyvis/edge.py
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class Edge(object):
|
2 |
+
def __init__(self, source, dest, directed=False, **options):
|
3 |
+
self.options = options
|
4 |
+
self.options["from"] = source
|
5 |
+
self.options["to"] = dest
|
6 |
+
if directed:
|
7 |
+
if "arrows" not in self.options:
|
8 |
+
self.options["arrows"] = "to"
|
pyvis/pyvis/network.py
ADDED
@@ -0,0 +1,984 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import os
|
3 |
+
import platform
|
4 |
+
import shutil
|
5 |
+
import tempfile
|
6 |
+
import webbrowser
|
7 |
+
from collections import defaultdict
|
8 |
+
from platform import uname
|
9 |
+
|
10 |
+
import jsonpickle
|
11 |
+
import networkx as nx
|
12 |
+
from IPython.core.display import HTML
|
13 |
+
from IPython.display import IFrame
|
14 |
+
from jinja2 import Template
|
15 |
+
|
16 |
+
from .edge import Edge
|
17 |
+
from .node import Node
|
18 |
+
from .options import Configure, Options
|
19 |
+
from .utils import check_html
|
20 |
+
|
21 |
+
|
22 |
+
class Network(object):
|
23 |
+
"""
|
24 |
+
The Network class is the focus of this library. All viz functionality
|
25 |
+
should be implemented off of a Network instance.
|
26 |
+
|
27 |
+
To instantiate:
|
28 |
+
|
29 |
+
>>> nt = Network()
|
30 |
+
"""
|
31 |
+
|
32 |
+
def __init__(
|
33 |
+
self,
|
34 |
+
height="500px",
|
35 |
+
width="100%",
|
36 |
+
directed=False,
|
37 |
+
notebook=False,
|
38 |
+
neighborhood_highlight=False,
|
39 |
+
select_menu=False,
|
40 |
+
bgcolor="#ffffff",
|
41 |
+
font_color=False,
|
42 |
+
layout=None,
|
43 |
+
heading="",
|
44 |
+
):
|
45 |
+
"""
|
46 |
+
:param height: The height of the canvas
|
47 |
+
:param width: The width of the canvas
|
48 |
+
:param directed: Whether or not to use a directed graph. This is false
|
49 |
+
by default.
|
50 |
+
:param notebook: True if using jupyter notebook.
|
51 |
+
:param bgcolor: The background color of the canvas.
|
52 |
+
:font_color: The color of the node labels text
|
53 |
+
:layout: Use hierarchical layout if this is set
|
54 |
+
|
55 |
+
:type height: num or str
|
56 |
+
:type width: num or str
|
57 |
+
:type directed: bool
|
58 |
+
:type notebook: bool
|
59 |
+
:type bgcolor: str
|
60 |
+
:type font_color: str
|
61 |
+
:type layout: bool
|
62 |
+
"""
|
63 |
+
self.nodes = []
|
64 |
+
self.edges = []
|
65 |
+
self.height = height
|
66 |
+
self.width = width
|
67 |
+
self.heading = heading
|
68 |
+
self.html = ""
|
69 |
+
self.shape = "dot"
|
70 |
+
self.font_color = font_color
|
71 |
+
self.directed = directed
|
72 |
+
self.bgcolor = bgcolor
|
73 |
+
self.use_DOT = False
|
74 |
+
self.dot_lang = ""
|
75 |
+
self.options = Options(layout)
|
76 |
+
self.widget = False
|
77 |
+
self.node_ids = []
|
78 |
+
self.node_map = {}
|
79 |
+
self.template = None
|
80 |
+
self.conf = False
|
81 |
+
self.path = os.path.dirname(__file__) + "/templates/template.html"
|
82 |
+
self.neighborhood_highlight = neighborhood_highlight
|
83 |
+
self.select_menu = select_menu
|
84 |
+
|
85 |
+
if notebook:
|
86 |
+
self.prep_notebook()
|
87 |
+
|
88 |
+
def __str__(self):
|
89 |
+
"""
|
90 |
+
override print to show readable graph data
|
91 |
+
"""
|
92 |
+
return str(
|
93 |
+
json.dumps(
|
94 |
+
{
|
95 |
+
"Nodes": self.node_ids,
|
96 |
+
"Edges": self.edges,
|
97 |
+
"Height": self.height,
|
98 |
+
"Width": self.width,
|
99 |
+
"Heading": self.heading,
|
100 |
+
},
|
101 |
+
indent=4,
|
102 |
+
)
|
103 |
+
)
|
104 |
+
|
105 |
+
def __repr__(self):
|
106 |
+
return "{} |N|={} |E|={:,}".format(
|
107 |
+
self.__class__, self.num_nodes(), self.num_edges()
|
108 |
+
)
|
109 |
+
|
110 |
+
def add_node(self, n_id, label=None, shape="dot", **options):
|
111 |
+
"""
|
112 |
+
This method adds a node to the network, given a mandatory node ID.
|
113 |
+
Node labels default to node ids if no label is specified during the
|
114 |
+
call.
|
115 |
+
|
116 |
+
>>> nt = Network("500px", "500px")
|
117 |
+
>>> nt.add_node(0, label="Node 0")
|
118 |
+
>>> nt.add_node(1, label="Node 1", color = "blue")
|
119 |
+
|
120 |
+
:param n_id: The id of the node. The id is mandatory for nodes and
|
121 |
+
they have to be unique. This should obviously be set per
|
122 |
+
node, not globally.
|
123 |
+
|
124 |
+
:param label: The label is the piece of text shown in or under the
|
125 |
+
node, depending on the shape.
|
126 |
+
|
127 |
+
:param borderWidth: The width of the border of the node.
|
128 |
+
|
129 |
+
:param borderWidthSelected: The width of the border of the node when
|
130 |
+
it is selected. When undefined, the
|
131 |
+
borderWidth * 2 is used.
|
132 |
+
|
133 |
+
:param brokenImage: When the shape is set to image or circularImage,
|
134 |
+
this option can be an URL to a backup image in
|
135 |
+
case the URL supplied in the image option cannot
|
136 |
+
be resolved.
|
137 |
+
|
138 |
+
:param group: When not undefined, the node will belong to the defined
|
139 |
+
group. Styling information of that group will apply to
|
140 |
+
this node. Node specific styling overrides group styling.
|
141 |
+
|
142 |
+
:param hidden: When true, the node will not be shown. It will still be
|
143 |
+
part of the physics simulation though!
|
144 |
+
|
145 |
+
:param image: When the shape is set to image or circularImage, this
|
146 |
+
option should be the URL to an image. If the image
|
147 |
+
cannot be found, the brokenImage option can be used.
|
148 |
+
|
149 |
+
:param labelHighlightBold: Determines whether or not the label becomes
|
150 |
+
bold when the node is selected.
|
151 |
+
|
152 |
+
:param level: When using the hierarchical layout, the level determines
|
153 |
+
where the node is going to be positioned.
|
154 |
+
|
155 |
+
:param mass: The barnesHut physics model (which is enabled by default)
|
156 |
+
is based on an inverted gravity model. By increasing
|
157 |
+
the mass of a node, you increase it's repulsion. Values
|
158 |
+
lower than 1 are not recommended.
|
159 |
+
|
160 |
+
:param physics: When false, the node is not part of the physics
|
161 |
+
simulation. It will not move except for from
|
162 |
+
manual dragging.
|
163 |
+
|
164 |
+
:param shape: The shape defines what the node looks like. There are
|
165 |
+
two types of nodes. One type has the label inside of
|
166 |
+
it and the other type has the label underneath it. The
|
167 |
+
types with the label inside of it are: ellipse, circle,
|
168 |
+
database, box, text. The ones with the label outside of
|
169 |
+
it are: image, circularImage, diamond, dot, star,
|
170 |
+
triangle, triangleDown, square and icon.
|
171 |
+
|
172 |
+
:param size: The size is used to determine the size of node shapes that
|
173 |
+
do not have the label inside of them. These shapes are:
|
174 |
+
image, circularImage, diamond, dot, star, triangle,
|
175 |
+
triangleDown, square and icon.
|
176 |
+
|
177 |
+
:param title: Title to be displayed when the user hovers over the node.
|
178 |
+
The title can be an HTML element or a string containing
|
179 |
+
plain text or HTML.
|
180 |
+
|
181 |
+
:param value: When a value is set, the nodes will be scaled using the
|
182 |
+
options in the scaling object defined above.
|
183 |
+
|
184 |
+
:param x: This gives a node an initial x position. When using the
|
185 |
+
hierarchical layout, either the x or y position is set by the
|
186 |
+
layout engine depending on the type of view. The other value
|
187 |
+
remains untouched. When using stabilization, the stabilized
|
188 |
+
position may be different from the initial one. To lock the
|
189 |
+
node to that position use the physics or fixed options.
|
190 |
+
|
191 |
+
:param y: This gives a node an initial y position. When using the
|
192 |
+
hierarchical layout,either the x or y position is set by
|
193 |
+
the layout engine depending on the type of view. The
|
194 |
+
other value remains untouched. When using stabilization,
|
195 |
+
the stabilized position may be different from the initial
|
196 |
+
one. To lock the node to that position use the physics or
|
197 |
+
fixed options.
|
198 |
+
|
199 |
+
:type n_id: str or int
|
200 |
+
:type label: str or int
|
201 |
+
:type borderWidth: num (optional)
|
202 |
+
:type borderWidthSelected: num (optional)
|
203 |
+
:type brokenImage: str (optional)
|
204 |
+
:type group: str (optional)
|
205 |
+
:type hidden: bool (optional)
|
206 |
+
:type image: str (optional)
|
207 |
+
:type labelHighlightBold: bool (optional)
|
208 |
+
:type level: num (optional)
|
209 |
+
:type mass: num (optional)
|
210 |
+
:type physics: bool (optional)
|
211 |
+
:type shape: str (optional)
|
212 |
+
:type size: num (optional)
|
213 |
+
:type title: str or html element (optional)
|
214 |
+
:type value: num (optional)
|
215 |
+
:type x: num (optional)
|
216 |
+
:type y: num (optional)
|
217 |
+
"""
|
218 |
+
assert isinstance(n_id, str) or isinstance(n_id, int)
|
219 |
+
if label:
|
220 |
+
node_label = label
|
221 |
+
else:
|
222 |
+
node_label = n_id
|
223 |
+
if n_id not in self.node_ids:
|
224 |
+
|
225 |
+
n = Node(
|
226 |
+
n_id, shape, label=node_label, font_color=self.font_color, **options
|
227 |
+
)
|
228 |
+
|
229 |
+
# if "group" in options:
|
230 |
+
# n = Node(n_id, shape, label=node_label, font_color=self.font_color, **options)
|
231 |
+
# else:
|
232 |
+
# n = Node(n_id, shape, label=node_label, color=color, font_color=self.font_color, **options)
|
233 |
+
|
234 |
+
self.nodes.append(n.options)
|
235 |
+
self.node_ids.append(n_id)
|
236 |
+
self.node_map[n_id] = n.options
|
237 |
+
|
238 |
+
def add_nodes(self, nodes, **kwargs):
|
239 |
+
"""
|
240 |
+
This method adds multiple nodes to the network from a list.
|
241 |
+
Default behavior uses values of 'nodes' for node ID and node label
|
242 |
+
properties. You can also specify other lists of properties to go
|
243 |
+
along each node.
|
244 |
+
|
245 |
+
Example:
|
246 |
+
|
247 |
+
>>> g = net.Network()
|
248 |
+
>>> g.add_nodes([1, 2, 3], size=[2, 4, 6], title=["n1", "n2", "n3"])
|
249 |
+
>>> g.nodes
|
250 |
+
>>> [{'id': 1, 'label': 1, 'shape': 'dot', 'size': 2, 'title': 'n1'},
|
251 |
+
|
252 |
+
Output:
|
253 |
+
|
254 |
+
>>> {'id': 2, 'label': 2, 'shape': 'dot', 'size': 4, 'title': 'n2'},
|
255 |
+
>>> {'id': 3, 'label': 3, 'shape': 'dot', 'size': 6, 'title': 'n3'}]
|
256 |
+
|
257 |
+
|
258 |
+
:param nodes: A list of nodes.
|
259 |
+
|
260 |
+
:type nodes: list
|
261 |
+
"""
|
262 |
+
valid_args = ["size", "value", "title", "x", "y", "label", "color", "shape"]
|
263 |
+
for k in kwargs:
|
264 |
+
assert k in valid_args, "invalid arg '" + k + "'"
|
265 |
+
|
266 |
+
nd = defaultdict(dict)
|
267 |
+
for i in range(len(nodes)):
|
268 |
+
for k, v in kwargs.items():
|
269 |
+
assert len(v) == len(nodes), (
|
270 |
+
"keyword arg %s [length %s] does not match"
|
271 |
+
"[length %s] of nodes" % (k, len(v), len(nodes))
|
272 |
+
)
|
273 |
+
nd[nodes[i]].update({k: v[i]})
|
274 |
+
|
275 |
+
for node in nodes:
|
276 |
+
# check if node is `number-like`
|
277 |
+
try:
|
278 |
+
node = int(node)
|
279 |
+
self.add_node(node, **nd[node])
|
280 |
+
except:
|
281 |
+
# or node could be string
|
282 |
+
assert isinstance(node, str)
|
283 |
+
self.add_node(node, **nd[node])
|
284 |
+
|
285 |
+
def num_nodes(self):
|
286 |
+
"""
|
287 |
+
Return number of nodes
|
288 |
+
|
289 |
+
:returns: :py:class:`int`
|
290 |
+
"""
|
291 |
+
return len(self.node_ids)
|
292 |
+
|
293 |
+
def num_edges(self):
|
294 |
+
"""
|
295 |
+
Return number of edges
|
296 |
+
|
297 |
+
:returns: :py:class:`int`
|
298 |
+
"""
|
299 |
+
return len(self.edges)
|
300 |
+
|
301 |
+
def add_edge(self, source, to, **options):
|
302 |
+
"""
|
303 |
+
|
304 |
+
Adding edges is done based off of the IDs of the nodes. Order does
|
305 |
+
not matter unless dealing with a directed graph.
|
306 |
+
|
307 |
+
>>> nt.add_edge(0, 1) # adds an edge from node ID 0 to node ID
|
308 |
+
>>> nt.add_edge(0, 1, value = 4) # adds an edge with a width of 4
|
309 |
+
|
310 |
+
|
311 |
+
:param arrowStrikethrough: When false, the edge stops at the arrow.
|
312 |
+
This can be useful if you have thick lines
|
313 |
+
and you want the arrow to end in a point.
|
314 |
+
Middle arrows are not affected by this.
|
315 |
+
|
316 |
+
:param from: Edges are between two nodes, one to and one from. This
|
317 |
+
is where you define the from node. You have to supply
|
318 |
+
the corresponding node ID. This naturally only applies
|
319 |
+
to individual edges.
|
320 |
+
|
321 |
+
:param hidden: When true, the edge is not drawn. It is part still part
|
322 |
+
of the physics simulation however!
|
323 |
+
|
324 |
+
:param physics: When true, the edge is part of the physics simulation.
|
325 |
+
When false, it will not act as a spring.
|
326 |
+
|
327 |
+
:param title: The title is shown in a pop-up when the mouse moves over
|
328 |
+
the edge.
|
329 |
+
|
330 |
+
:param to: Edges are between two nodes, one to and one from. This is
|
331 |
+
where you define the to node. You have to supply the
|
332 |
+
corresponding node ID. This naturally only applies to
|
333 |
+
individual edges.
|
334 |
+
|
335 |
+
:param value: When a value is set, the edges' width will be scaled
|
336 |
+
using the options in the scaling object defined above.
|
337 |
+
|
338 |
+
:param width: The width of the edge. If value is set, this is not used.
|
339 |
+
|
340 |
+
|
341 |
+
:type arrowStrikethrough: bool
|
342 |
+
:type from: str or num
|
343 |
+
:type hidden: bool
|
344 |
+
:type physics: bool
|
345 |
+
:type title: str
|
346 |
+
:type to: str or num
|
347 |
+
:type value: num
|
348 |
+
:type width: num
|
349 |
+
"""
|
350 |
+
edge_exists = False
|
351 |
+
|
352 |
+
# verify nodes exists
|
353 |
+
assert source in self.get_nodes(), "non existent node '" + str(source) + "'"
|
354 |
+
|
355 |
+
assert to in self.get_nodes(), "non existent node '" + str(to) + "'"
|
356 |
+
|
357 |
+
# we only check existing edge for undirected graphs
|
358 |
+
if not self.directed:
|
359 |
+
for e in self.edges:
|
360 |
+
frm = e["from"]
|
361 |
+
dest = e["to"]
|
362 |
+
if (source == dest and to == frm) or (source == frm and to == dest):
|
363 |
+
# edge already exists
|
364 |
+
edge_exists = True
|
365 |
+
|
366 |
+
if not edge_exists:
|
367 |
+
e = Edge(source, to, self.directed, **options)
|
368 |
+
self.edges.append(e.options)
|
369 |
+
|
370 |
+
def add_edges(self, edges):
|
371 |
+
"""
|
372 |
+
This method serves to add multiple edges between existing nodes
|
373 |
+
in the network instance. Adding of the edges is done based off
|
374 |
+
of the IDs of the nodes. Order does not matter unless dealing with a
|
375 |
+
directed graph.
|
376 |
+
|
377 |
+
:param edges: A list of tuples, each tuple consists of source of edge,
|
378 |
+
edge destination and and optional width.
|
379 |
+
|
380 |
+
:type arrowStrikethrough: list of tuples
|
381 |
+
"""
|
382 |
+
for edge in edges:
|
383 |
+
# if incoming tuple contains a weight
|
384 |
+
if len(edge) == 3:
|
385 |
+
self.add_edge(edge[0], edge[1], width=edge[2])
|
386 |
+
else:
|
387 |
+
self.add_edge(edge[0], edge[1])
|
388 |
+
|
389 |
+
def get_network_data(self):
|
390 |
+
"""
|
391 |
+
Extract relevant information about this network in order to inject into
|
392 |
+
a Jinja2 template.
|
393 |
+
|
394 |
+
Returns:
|
395 |
+
nodes (list), edges (list), height (
|
396 |
+
string), width (string), options (object)
|
397 |
+
|
398 |
+
Usage:
|
399 |
+
|
400 |
+
>>> nodes, edges, heading, height, width, options = net.get_network_data()
|
401 |
+
"""
|
402 |
+
if isinstance(self.options, dict):
|
403 |
+
return (
|
404 |
+
self.nodes,
|
405 |
+
self.edges,
|
406 |
+
self.heading,
|
407 |
+
self.height,
|
408 |
+
self.width,
|
409 |
+
json.dumps(self.options),
|
410 |
+
)
|
411 |
+
else:
|
412 |
+
return (
|
413 |
+
self.nodes,
|
414 |
+
self.edges,
|
415 |
+
self.heading,
|
416 |
+
self.height,
|
417 |
+
self.width,
|
418 |
+
self.options.to_json(),
|
419 |
+
)
|
420 |
+
|
421 |
+
def save_graph(self, name):
|
422 |
+
"""
|
423 |
+
Save the graph as html in the current directory with name.
|
424 |
+
|
425 |
+
:param name: the name of the html file to save as
|
426 |
+
:type name: str
|
427 |
+
"""
|
428 |
+
check_html(name)
|
429 |
+
self.write_html(name)
|
430 |
+
|
431 |
+
def generate_html(self, notebook=False):
|
432 |
+
"""
|
433 |
+
This method generates HTML from the data structures supporting the nodes, edges,
|
434 |
+
and options and updates the template to generate the HTML holding
|
435 |
+
the visualization.
|
436 |
+
|
437 |
+
:type notebook: bool
|
438 |
+
|
439 |
+
Returns
|
440 |
+
:type out: str
|
441 |
+
"""
|
442 |
+
|
443 |
+
# here, check if an href is present in the hover data
|
444 |
+
use_link_template = False
|
445 |
+
for n in self.nodes:
|
446 |
+
title = n.get("title", None)
|
447 |
+
if title:
|
448 |
+
if "href" in title:
|
449 |
+
"""
|
450 |
+
this tells the template to override default hover
|
451 |
+
mechanic, as the tooltip would move with the mouse
|
452 |
+
cursor which made interacting with hover data useless.
|
453 |
+
"""
|
454 |
+
use_link_template = True
|
455 |
+
break
|
456 |
+
if not notebook:
|
457 |
+
with open(self.path) as html:
|
458 |
+
content = html.read()
|
459 |
+
template = Template(content)
|
460 |
+
else:
|
461 |
+
template = self.template
|
462 |
+
|
463 |
+
nodes, edges, heading, height, width, options = self.get_network_data()
|
464 |
+
|
465 |
+
# check if physics is enabled
|
466 |
+
if isinstance(self.options, dict):
|
467 |
+
if "physics" in self.options and "enabled" in self.options["physics"]:
|
468 |
+
physics_enabled = self.options["physics"]["enabled"]
|
469 |
+
else:
|
470 |
+
physics_enabled = True
|
471 |
+
else:
|
472 |
+
physics_enabled = self.options.physics.enabled
|
473 |
+
|
474 |
+
out = template.render(
|
475 |
+
height=height,
|
476 |
+
width=width,
|
477 |
+
nodes=nodes,
|
478 |
+
edges=edges,
|
479 |
+
heading=heading,
|
480 |
+
options=options,
|
481 |
+
physics_enabled=physics_enabled,
|
482 |
+
use_DOT=self.use_DOT,
|
483 |
+
dot_lang=self.dot_lang,
|
484 |
+
widget=self.widget,
|
485 |
+
bgcolor=self.bgcolor,
|
486 |
+
conf=self.conf,
|
487 |
+
tooltip_link=use_link_template,
|
488 |
+
neighborhood_highlight=self.neighborhood_highlight,
|
489 |
+
select_menu=self.select_menu,
|
490 |
+
)
|
491 |
+
|
492 |
+
return out
|
493 |
+
|
494 |
+
def write_html(self, name="index.html", local=True, notebook=False):
|
495 |
+
"""
|
496 |
+
This method gets the data structures supporting the nodes, edges,
|
497 |
+
and options and updates the template to write the HTML holding
|
498 |
+
the visualization.
|
499 |
+
:type name_html: str
|
500 |
+
"""
|
501 |
+
check_html(name)
|
502 |
+
self.html = self.generate_html(notebook=notebook)
|
503 |
+
|
504 |
+
if notebook:
|
505 |
+
if os.path.exists("lib"):
|
506 |
+
shutil.rmtree(f"lib")
|
507 |
+
shutil.copytree(f"{os.path.dirname(__file__)}/lib", "lib")
|
508 |
+
with open(name, "w+") as out:
|
509 |
+
out.write(self.html)
|
510 |
+
return IFrame(name, width=self.width, height="600px")
|
511 |
+
else:
|
512 |
+
if local:
|
513 |
+
tempdir = "."
|
514 |
+
else:
|
515 |
+
tempdir = tempfile.mkdtemp()
|
516 |
+
# with tempfile.mkdtemp() as tempdir:
|
517 |
+
if os.path.exists(f"{tempdir}/lib"):
|
518 |
+
shutil.rmtree(f"{tempdir}/lib")
|
519 |
+
shutil.copytree(f"{os.path.dirname(__file__)}/lib", f"{tempdir}/lib")
|
520 |
+
|
521 |
+
with open(f"{tempdir}/{name}", "w+") as out:
|
522 |
+
out.write(self.html)
|
523 |
+
webbrowser.open(f"{tempdir}/{name}")
|
524 |
+
|
525 |
+
def show(self, name, local=True):
|
526 |
+
"""
|
527 |
+
Writes a static HTML file and saves it locally before opening.
|
528 |
+
|
529 |
+
:param: name: the name of the html file to save as
|
530 |
+
:type name: str
|
531 |
+
"""
|
532 |
+
check_html(name)
|
533 |
+
if self.template is not None:
|
534 |
+
return self.write_html(name, local, notebook=True)
|
535 |
+
else:
|
536 |
+
self.write_html(name, local)
|
537 |
+
# webbrowser.open(name)
|
538 |
+
|
539 |
+
def prep_notebook(self, custom_template=False, custom_template_path=None):
|
540 |
+
"""
|
541 |
+
Loads the template data into the template attribute of the network.
|
542 |
+
This should be done in a jupyter notebook environment before showing
|
543 |
+
the network.
|
544 |
+
|
545 |
+
Example:
|
546 |
+
>>> net.prep_notebook()
|
547 |
+
>>> net.show("nb.html")
|
548 |
+
|
549 |
+
|
550 |
+
:param path: the relative path pointing to a template html file
|
551 |
+
:type path: string
|
552 |
+
"""
|
553 |
+
if custom_template and custom_template_path:
|
554 |
+
self.set_template(custom_template_path)
|
555 |
+
with open(self.path) as html:
|
556 |
+
content = html.read()
|
557 |
+
self.template = Template(content)
|
558 |
+
|
559 |
+
def set_template(self, path_to_template):
|
560 |
+
self.path = path_to_template
|
561 |
+
|
562 |
+
def from_DOT(self, dot):
|
563 |
+
"""
|
564 |
+
This method takes the contents of .DOT file and converts it
|
565 |
+
to a PyVis visualization.
|
566 |
+
|
567 |
+
Assuming the contents of test.dot contains:
|
568 |
+
digraph sample3 {
|
569 |
+
A -> {B ; C ; D}
|
570 |
+
C -> {B ; A}
|
571 |
+
}
|
572 |
+
|
573 |
+
Usage:
|
574 |
+
|
575 |
+
>>> nt.Network("500px", "500px")
|
576 |
+
>>> nt.from_DOT("test.dot")
|
577 |
+
>>> nt.show("dot.html")
|
578 |
+
|
579 |
+
:param dot: The path of the dotfile being converted.
|
580 |
+
:type dot: .dot file
|
581 |
+
|
582 |
+
"""
|
583 |
+
self.use_DOT = True
|
584 |
+
file = open(dot, "r")
|
585 |
+
s = str(file.read())
|
586 |
+
self.dot_lang = " ".join(s.splitlines())
|
587 |
+
self.dot_lang = self.dot_lang.replace('"', '\\"')
|
588 |
+
|
589 |
+
def get_adj_list(self):
|
590 |
+
"""
|
591 |
+
This method returns the user an adjacency list representation
|
592 |
+
of the network.
|
593 |
+
|
594 |
+
:returns: dictionary mapping of Node ID to list of Node IDs it
|
595 |
+
is connected to.
|
596 |
+
"""
|
597 |
+
a_list = {}
|
598 |
+
for i in self.nodes:
|
599 |
+
a_list[i["id"]] = set()
|
600 |
+
if self.directed:
|
601 |
+
for e in self.edges:
|
602 |
+
source = e["from"]
|
603 |
+
dest = e["to"]
|
604 |
+
a_list[source].add(dest)
|
605 |
+
else:
|
606 |
+
for e in self.edges:
|
607 |
+
source = e["from"]
|
608 |
+
dest = e["to"]
|
609 |
+
if dest not in a_list[source] and source not in a_list[dest]:
|
610 |
+
a_list[source].add(dest)
|
611 |
+
a_list[dest].add(source)
|
612 |
+
return a_list
|
613 |
+
|
614 |
+
def neighbors(self, node):
|
615 |
+
"""
|
616 |
+
Given a node id, return the set of neighbors of this particular node.
|
617 |
+
|
618 |
+
:param node: The node to get the neighbors from
|
619 |
+
:type node: str or int
|
620 |
+
|
621 |
+
:returns: set
|
622 |
+
"""
|
623 |
+
assert isinstance(node, str) or isinstance(
|
624 |
+
node, int
|
625 |
+
), "error: expected int or str for node but got %s" % type(node)
|
626 |
+
assert node in self.node_ids, "error: %s node not in network" % node
|
627 |
+
return self.get_adj_list()[node]
|
628 |
+
|
629 |
+
def from_nx(
|
630 |
+
self,
|
631 |
+
nx_graph,
|
632 |
+
node_size_transf=(lambda x: x),
|
633 |
+
edge_weight_transf=(lambda x: x),
|
634 |
+
default_node_size=10,
|
635 |
+
default_edge_weight=1,
|
636 |
+
show_edge_weights=True,
|
637 |
+
edge_scaling=False,
|
638 |
+
):
|
639 |
+
"""
|
640 |
+
This method takes an exisitng Networkx graph and translates
|
641 |
+
it to a PyVis graph format that can be accepted by the VisJs
|
642 |
+
API in the Jinja2 template. This operation is done in place.
|
643 |
+
|
644 |
+
:param nx_graph: The Networkx graph object that is to be translated.
|
645 |
+
:type nx_graph: networkx.Graph instance
|
646 |
+
:param node_size_transf: function to transform the node size for plotting
|
647 |
+
:type node_size_transf: func
|
648 |
+
:param edge_weight_transf: function to transform the edge weight for plotting
|
649 |
+
:type edge_weight_transf: func
|
650 |
+
:param default_node_size: default node size if not specified
|
651 |
+
:param default_edge_weight: default edge weight if not specified
|
652 |
+
>>> nx_graph = nx.cycle_graph(10)
|
653 |
+
>>> nx_graph.nodes[1]['title'] = 'Number 1'
|
654 |
+
>>> nx_graph.nodes[1]['group'] = 1
|
655 |
+
>>> nx_graph.nodes[3]['title'] = 'I belong to a different group!'
|
656 |
+
>>> nx_graph.nodes[3]['group'] = 10
|
657 |
+
>>> nx_graph.add_node(20, size=20, title='couple', group=2)
|
658 |
+
>>> nx_graph.add_node(21, size=15, title='couple', group=2)
|
659 |
+
>>> nx_graph.add_edge(20, 21, weight=5)
|
660 |
+
>>> nx_graph.add_node(25, size=25, label='lonely', title='lonely node', group=3)
|
661 |
+
>>> nt = Network("500px", "500px")
|
662 |
+
# populates the nodes and edges data structures
|
663 |
+
>>> nt.from_nx(nx_graph)
|
664 |
+
>>> nt.show("nx.html")
|
665 |
+
"""
|
666 |
+
assert isinstance(nx_graph, nx.Graph)
|
667 |
+
edges = nx_graph.edges(data=True)
|
668 |
+
nodes = nx_graph.nodes(data=True)
|
669 |
+
|
670 |
+
if len(edges) > 0:
|
671 |
+
for e in edges:
|
672 |
+
if "size" not in nodes[e[0]].keys():
|
673 |
+
nodes[e[0]]["size"] = default_node_size
|
674 |
+
nodes[e[0]]["size"] = int(node_size_transf(nodes[e[0]]["size"]))
|
675 |
+
if "size" not in nodes[e[1]].keys():
|
676 |
+
nodes[e[1]]["size"] = default_node_size
|
677 |
+
nodes[e[1]]["size"] = int(node_size_transf(nodes[e[1]]["size"]))
|
678 |
+
self.add_node(e[0], **nodes[e[0]])
|
679 |
+
self.add_node(e[1], **nodes[e[1]])
|
680 |
+
|
681 |
+
# if user does not pass a 'weight' argument
|
682 |
+
if "value" not in e[2] or "width" not in e[2]:
|
683 |
+
if edge_scaling:
|
684 |
+
width_type = "value"
|
685 |
+
else:
|
686 |
+
width_type = "width"
|
687 |
+
if "weight" not in e[2].keys():
|
688 |
+
e[2]["weight"] = default_edge_weight
|
689 |
+
e[2][width_type] = edge_weight_transf(e[2]["weight"])
|
690 |
+
# replace provided weight value and pass to 'value' or 'width'
|
691 |
+
e[2][width_type] = e[2].pop("weight")
|
692 |
+
|
693 |
+
self.add_edge(e[0], e[1], **e[2])
|
694 |
+
|
695 |
+
for node in nx.isolates(nx_graph):
|
696 |
+
if "size" not in nodes[node].keys():
|
697 |
+
nodes[node]["size"] = default_node_size
|
698 |
+
self.add_node(node, **nodes[node])
|
699 |
+
|
700 |
+
def get_nodes(self):
|
701 |
+
"""
|
702 |
+
This method returns an iterable list of node ids
|
703 |
+
|
704 |
+
:returns: list
|
705 |
+
"""
|
706 |
+
return self.node_ids
|
707 |
+
|
708 |
+
def get_node(self, n_id):
|
709 |
+
"""
|
710 |
+
Lookup node by ID and return it.
|
711 |
+
|
712 |
+
:param n_id: The ID given to the node.
|
713 |
+
|
714 |
+
:returns: dict containing node properties
|
715 |
+
"""
|
716 |
+
return self.node_map[n_id]
|
717 |
+
|
718 |
+
def get_edges(self):
|
719 |
+
"""
|
720 |
+
This method returns an iterable list of edge objects
|
721 |
+
|
722 |
+
:returns: list
|
723 |
+
"""
|
724 |
+
return self.edges
|
725 |
+
|
726 |
+
def barnes_hut(
|
727 |
+
self,
|
728 |
+
gravity=-80000,
|
729 |
+
central_gravity=0.3,
|
730 |
+
spring_length=250,
|
731 |
+
spring_strength=0.001,
|
732 |
+
damping=0.09,
|
733 |
+
overlap=0,
|
734 |
+
):
|
735 |
+
"""
|
736 |
+
BarnesHut is a quadtree based gravity model. It is the fastest. default
|
737 |
+
and recommended solver for non-hierarchical layouts.
|
738 |
+
|
739 |
+
:param gravity: The more negative the gravity value is, the stronger the
|
740 |
+
repulsion is.
|
741 |
+
:param central_gravity: The gravity attractor to pull the entire network
|
742 |
+
to the center.
|
743 |
+
:param spring_length: The rest length of the edges
|
744 |
+
:param spring_strength: The strong the edges springs are
|
745 |
+
:param damping: A value ranging from 0 to 1 of how much of the velocity
|
746 |
+
from the previous physics simulation iteration carries
|
747 |
+
over to the next iteration.
|
748 |
+
:param overlap: When larger than 0, the size of the node is taken into
|
749 |
+
account. The distance will be calculated from the radius
|
750 |
+
of the encompassing circle of the node for both the
|
751 |
+
gravity model. Value 1 is maximum overlap avoidance.
|
752 |
+
|
753 |
+
:type gravity: int
|
754 |
+
:type central_gravity: float
|
755 |
+
:type spring_length: int
|
756 |
+
:type spring_strength: float
|
757 |
+
:type damping: float
|
758 |
+
:type overlap: float
|
759 |
+
"""
|
760 |
+
self.options.physics.use_barnes_hut(locals())
|
761 |
+
|
762 |
+
def repulsion(
|
763 |
+
self,
|
764 |
+
node_distance=100,
|
765 |
+
central_gravity=0.2,
|
766 |
+
spring_length=200,
|
767 |
+
spring_strength=0.05,
|
768 |
+
damping=0.09,
|
769 |
+
):
|
770 |
+
"""
|
771 |
+
Set the physics attribute of the entire network to repulsion.
|
772 |
+
When called, it sets the solver attribute of physics to repulsion.
|
773 |
+
|
774 |
+
:param node_distance: This is the range of influence for the repulsion.
|
775 |
+
:param central_gravity: The gravity attractor to pull the entire network
|
776 |
+
to the center.
|
777 |
+
:param spring_length: The rest length of the edges
|
778 |
+
:param spring_strength: The strong the edges springs are
|
779 |
+
:param damping: A value ranging from 0 to 1 of how much of the velocity
|
780 |
+
from the previous physics simulation iteration carries
|
781 |
+
over to the next iteration.
|
782 |
+
|
783 |
+
:type node_distance: int
|
784 |
+
:type central_gravity float
|
785 |
+
:type spring_length: int
|
786 |
+
:type spring_strength: float
|
787 |
+
:type damping: float
|
788 |
+
"""
|
789 |
+
self.options.physics.use_repulsion(locals())
|
790 |
+
|
791 |
+
def hrepulsion(
|
792 |
+
self,
|
793 |
+
node_distance=120,
|
794 |
+
central_gravity=0.0,
|
795 |
+
spring_length=100,
|
796 |
+
spring_strength=0.01,
|
797 |
+
damping=0.09,
|
798 |
+
):
|
799 |
+
"""
|
800 |
+
This model is based on the repulsion solver but the levels are
|
801 |
+
taken into account and the forces are normalized.
|
802 |
+
|
803 |
+
:param node_distance: This is the range of influence for the repulsion.
|
804 |
+
:param central_gravity: The gravity attractor to pull the entire network
|
805 |
+
to the center.
|
806 |
+
:param spring_length: The rest length of the edges
|
807 |
+
:param spring_strength: The strong the edges springs are
|
808 |
+
:param damping: A value ranging from 0 to 1 of how much of the velocity
|
809 |
+
from the previous physics simulation iteration carries
|
810 |
+
over to the next iteration.
|
811 |
+
|
812 |
+
:type node_distance: int
|
813 |
+
:type central_gravity float
|
814 |
+
:type spring_length: int
|
815 |
+
:type spring_strength: float
|
816 |
+
:type damping: float
|
817 |
+
"""
|
818 |
+
self.options.physics.use_hrepulsion(locals())
|
819 |
+
|
820 |
+
def force_atlas_2based(
|
821 |
+
self,
|
822 |
+
gravity=-50,
|
823 |
+
central_gravity=0.01,
|
824 |
+
spring_length=100,
|
825 |
+
spring_strength=0.08,
|
826 |
+
damping=0.4,
|
827 |
+
overlap=0,
|
828 |
+
):
|
829 |
+
"""
|
830 |
+
The forceAtlas2Based solver makes use of some of the equations provided
|
831 |
+
by them and makes use of the barnesHut implementation in vis. The main
|
832 |
+
differences are the central gravity model, which is here distance
|
833 |
+
independent, and the repulsion being linear instead of quadratic. Finally,
|
834 |
+
all node masses have a multiplier based on the amount of connected edges
|
835 |
+
plus one.
|
836 |
+
|
837 |
+
:param gravity: The more negative the gravity value is, the stronger the
|
838 |
+
repulsion is.
|
839 |
+
:param central_gravity: The gravity attractor to pull the entire network
|
840 |
+
to the center.
|
841 |
+
:param spring_length: The rest length of the edges
|
842 |
+
:param spring_strength: The strong the edges springs are
|
843 |
+
:param damping: A value ranging from 0 to 1 of how much of the velocity
|
844 |
+
from the previous physics simulation iteration carries
|
845 |
+
over to the next iteration.
|
846 |
+
:param overlap: When larger than 0, the size of the node is taken into
|
847 |
+
account. The distance will be calculated from the radius
|
848 |
+
of the encompassing circle of the node for both the
|
849 |
+
gravity model. Value 1 is maximum overlap avoidance.
|
850 |
+
|
851 |
+
:type gravity: int
|
852 |
+
:type central_gravity: float
|
853 |
+
:type spring_length: int
|
854 |
+
:type spring_strength: float
|
855 |
+
:type damping: float
|
856 |
+
:type overlap: float
|
857 |
+
"""
|
858 |
+
self.options.physics.use_force_atlas_2based(locals())
|
859 |
+
|
860 |
+
def to_json(self, max_depth=1, **args):
|
861 |
+
return jsonpickle.encode(self, max_depth=max_depth, **args)
|
862 |
+
|
863 |
+
def set_edge_smooth(self, smooth_type):
|
864 |
+
"""
|
865 |
+
Sets the smooth.type attribute of the edges.
|
866 |
+
|
867 |
+
:param smooth_type: Possible options: 'dynamic', 'continuous',
|
868 |
+
'discrete', 'diagonalCross', 'straightCross',
|
869 |
+
'horizontal', 'vertical', 'curvedCW',
|
870 |
+
'curvedCCW', 'cubicBezier'.
|
871 |
+
When using dynamic, the edges will have an
|
872 |
+
invisible support node guiding the shape.
|
873 |
+
This node is part of the physics simulation.
|
874 |
+
Default is set to continous.
|
875 |
+
|
876 |
+
:type smooth_type: string
|
877 |
+
"""
|
878 |
+
self.options.edges.smooth.enabled = True
|
879 |
+
self.options.edges.smooth.type = smooth_type
|
880 |
+
|
881 |
+
def toggle_hide_edges_on_drag(self, status):
|
882 |
+
"""
|
883 |
+
Displays or hides edges while dragging the network. This makes
|
884 |
+
panning of the network easy.
|
885 |
+
|
886 |
+
:param status: True if edges should be hidden on drag
|
887 |
+
|
888 |
+
:type status: bool
|
889 |
+
"""
|
890 |
+
self.options.interaction.hideEdgesOnDrag = status
|
891 |
+
|
892 |
+
def toggle_hide_nodes_on_drag(self, status):
|
893 |
+
"""
|
894 |
+
Displays or hides nodes while dragging the network. This makes
|
895 |
+
panning of the network easy.
|
896 |
+
|
897 |
+
:param status: When set to True, the nodes will hide on drag.
|
898 |
+
Default is set to False.
|
899 |
+
|
900 |
+
:type status: bool
|
901 |
+
"""
|
902 |
+
self.options.interaction.hideNodesOnDrag = status
|
903 |
+
|
904 |
+
def inherit_edge_colors(self, status):
|
905 |
+
"""
|
906 |
+
Edges take on the color of the node they are coming from.
|
907 |
+
|
908 |
+
:param status: True if edges should adopt color coming from.
|
909 |
+
:type status: bool
|
910 |
+
"""
|
911 |
+
self.options.edges.inherit_colors(status)
|
912 |
+
|
913 |
+
def show_buttons(self, filter_=None):
|
914 |
+
"""
|
915 |
+
Displays or hides certain widgets to dynamically modify the
|
916 |
+
network.
|
917 |
+
|
918 |
+
Usage:
|
919 |
+
>>> g.show_buttons(filter_=['nodes', 'edges', 'physics'])
|
920 |
+
|
921 |
+
Or to show all options:
|
922 |
+
>>> g.show_buttons()
|
923 |
+
|
924 |
+
:param status: When set to True, the widgets will be shown.
|
925 |
+
Default is set to False.
|
926 |
+
:param filter_: Only include widgets specified by `filter_`.
|
927 |
+
Valid options: True (gives all widgets)
|
928 |
+
List of `nodes`, `edges`,
|
929 |
+
`layout`, `interaction`,
|
930 |
+
`manipulation`, `physics`,
|
931 |
+
`selection`, `renderer`.
|
932 |
+
|
933 |
+
:type status: bool
|
934 |
+
:type filter_: bool or list:
|
935 |
+
"""
|
936 |
+
self.conf = True
|
937 |
+
self.options.configure = Configure(enabled=True, filter_=filter_)
|
938 |
+
self.widget = True
|
939 |
+
|
940 |
+
def toggle_physics(self, status):
|
941 |
+
"""
|
942 |
+
Toggles physics simulation
|
943 |
+
|
944 |
+
:param status: When False, nodes are not part of the physics
|
945 |
+
simulation. They will not move except for from
|
946 |
+
manual dragging.
|
947 |
+
Default is set to True.
|
948 |
+
|
949 |
+
:type status: bool
|
950 |
+
"""
|
951 |
+
self.options.physics.enabled = status
|
952 |
+
|
953 |
+
def toggle_drag_nodes(self, status):
|
954 |
+
"""
|
955 |
+
Toggles the dragging of the nodes in the network.
|
956 |
+
|
957 |
+
:param status: When set to True, the nodes can be dragged around
|
958 |
+
in the network. Default is set to True.
|
959 |
+
|
960 |
+
:type status: bool
|
961 |
+
"""
|
962 |
+
self.options.interaction.dragNodes = status
|
963 |
+
|
964 |
+
def toggle_stabilization(self, status):
|
965 |
+
"""
|
966 |
+
Toggles the stablization of the network.
|
967 |
+
|
968 |
+
:param status: Default is set to True.
|
969 |
+
|
970 |
+
:type status: bool
|
971 |
+
"""
|
972 |
+
self.options.physics.toggle_stabilization(status)
|
973 |
+
|
974 |
+
def set_options(self, options):
|
975 |
+
"""
|
976 |
+
Overrides the default options object passed to the VisJS framework.
|
977 |
+
Delegates to the :meth:`options.Options.set` routine.
|
978 |
+
|
979 |
+
:param options: The string representation of the Javascript-like object
|
980 |
+
to be used to override default options.
|
981 |
+
|
982 |
+
:type options: str
|
983 |
+
"""
|
984 |
+
self.options = self.options.set(options)
|
pyvis/pyvis/node.py
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class Node(object):
|
2 |
+
def __init__(self, n_id, shape, label, font_color=False, **opts):
|
3 |
+
self.options = opts
|
4 |
+
self.options["id"] = n_id
|
5 |
+
self.options["label"] = label
|
6 |
+
self.options["shape"] = shape
|
7 |
+
if font_color:
|
8 |
+
self.options["font"] = dict(color=font_color)
|
pyvis/pyvis/options.py
ADDED
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from .physics import *
|
2 |
+
|
3 |
+
|
4 |
+
class EdgeOptions(object):
|
5 |
+
"""
|
6 |
+
This is where the construction of the edges' options takes place.
|
7 |
+
So far, the edge smoothness can be switched through this object
|
8 |
+
as well as the edge color's inheritance.
|
9 |
+
"""
|
10 |
+
|
11 |
+
def __init__(self):
|
12 |
+
self.smooth = self.Smooth()
|
13 |
+
self.color = self.Color()
|
14 |
+
|
15 |
+
def inherit_colors(self, status):
|
16 |
+
"""
|
17 |
+
Whether or not to inherit colors from the source node.
|
18 |
+
If this is set to `from` then the edge will take the color
|
19 |
+
of the source node. If it is set to `to` then the color will
|
20 |
+
be that of the destination node.
|
21 |
+
|
22 |
+
.. note:: If set to `True` then the `from` behavior is adopted
|
23 |
+
and vice versa.
|
24 |
+
"""
|
25 |
+
self.color.inherit = status
|
26 |
+
|
27 |
+
def toggle_smoothness(self, smooth_type):
|
28 |
+
"""
|
29 |
+
Change smooth option for edges. When using dynamic, the edges will
|
30 |
+
have an invisible support node guiding the shape. This node is part
|
31 |
+
of the physics simulation,
|
32 |
+
|
33 |
+
:param smooth_type: Possible options are dynamic, continuous, discrete,
|
34 |
+
diagonalCross, straightCross, horizontal, vertical,
|
35 |
+
curvedCW, curvedCCW, cubicBezier
|
36 |
+
|
37 |
+
:type smooth_type: str
|
38 |
+
"""
|
39 |
+
self.smooth.type = smooth_type
|
40 |
+
|
41 |
+
def __repr__(self):
|
42 |
+
return str(self.__dict__)
|
43 |
+
|
44 |
+
class Smooth(object):
|
45 |
+
"""
|
46 |
+
When the edges are made to be smooth, the edges are drawn as a
|
47 |
+
dynamic quadratic bezier curve. The drawing of these curves
|
48 |
+
takes longer than that of the straight curves but it looks better.
|
49 |
+
There is a difference between dynamic smooth curves and static
|
50 |
+
smooth curves. The dynamic smooth curves have an invisible support
|
51 |
+
node that takes part in the physics simulation. If there are a lot
|
52 |
+
of edges, another kind of smooth than dynamic would be better for
|
53 |
+
performance.
|
54 |
+
"""
|
55 |
+
|
56 |
+
def __repr__(self):
|
57 |
+
return str(self.__dict__)
|
58 |
+
|
59 |
+
def __init__(self):
|
60 |
+
self.enabled = True
|
61 |
+
self.type = "dynamic"
|
62 |
+
|
63 |
+
class Color(object):
|
64 |
+
"""
|
65 |
+
The color object contains the color information of the edge
|
66 |
+
in every situation. When the edge only needs a single color value
|
67 |
+
like 'rgb(120,32,14)', '#ffffff' or 'red' can be supplied instead
|
68 |
+
of an object.
|
69 |
+
"""
|
70 |
+
|
71 |
+
def __repr__(self):
|
72 |
+
return str(self.__dict__)
|
73 |
+
|
74 |
+
def __init__(self):
|
75 |
+
self.inherit = True
|
76 |
+
|
77 |
+
|
78 |
+
class Interaction(object):
|
79 |
+
"""
|
80 |
+
Used for all user interaction with the network. Handles mouse
|
81 |
+
and touch events as well as the navigation buttons and the popups.
|
82 |
+
"""
|
83 |
+
|
84 |
+
def __repr__(self):
|
85 |
+
return str(self.__dict__)
|
86 |
+
|
87 |
+
def __init__(self):
|
88 |
+
self.hideEdgesOnDrag = False
|
89 |
+
self.hideNodesOnDrag = False
|
90 |
+
self.dragNodes = True
|
91 |
+
|
92 |
+
def __getitem__(self, item):
|
93 |
+
return self.__dict__[item]
|
94 |
+
|
95 |
+
|
96 |
+
class Configure(object):
|
97 |
+
"""
|
98 |
+
Handles the HTML part of the canvas and generates
|
99 |
+
an interactive option editor with filtering.
|
100 |
+
"""
|
101 |
+
|
102 |
+
def __repr__(self):
|
103 |
+
return str(self.__dict__)
|
104 |
+
|
105 |
+
def __init__(self, enabled=False, filter_=None):
|
106 |
+
self.enabled = enabled
|
107 |
+
if filter_:
|
108 |
+
self.filter = filter_
|
109 |
+
|
110 |
+
def __getitem__(self, item):
|
111 |
+
return self.__dict__[item]
|
112 |
+
|
113 |
+
|
114 |
+
class Layout(object):
|
115 |
+
"""
|
116 |
+
Acts as the camera that looks on the canvas.
|
117 |
+
Does the animation, zooming and focusing.
|
118 |
+
"""
|
119 |
+
|
120 |
+
def __repr__(self):
|
121 |
+
return str(self.__dict__)
|
122 |
+
|
123 |
+
def __init__(self, randomSeed=None, improvedLayout=True):
|
124 |
+
if not randomSeed:
|
125 |
+
self.randomSeed = 0
|
126 |
+
else:
|
127 |
+
self.randomSeed = randomSeed
|
128 |
+
self.improvedLayout = improvedLayout
|
129 |
+
self.hierarchical = self.Hierarchical(enabled=True)
|
130 |
+
|
131 |
+
def set_separation(self, distance):
|
132 |
+
"""
|
133 |
+
The distance between the different levels.
|
134 |
+
"""
|
135 |
+
self.hierarchical.levelSeparation = distance
|
136 |
+
|
137 |
+
def set_tree_spacing(self, distance):
|
138 |
+
"""
|
139 |
+
Distance between different trees (independent networks). This is
|
140 |
+
only for the initial layout. If you enable physics, the repulsion
|
141 |
+
model will denote the distance between the trees.
|
142 |
+
"""
|
143 |
+
self.hierarchical.treeSpacing = distance
|
144 |
+
|
145 |
+
def set_edge_minimization(self, status):
|
146 |
+
"""
|
147 |
+
Method for reducing whitespace. Can be used alone or together with
|
148 |
+
block shifting. Enabling block shifting will usually speed up the
|
149 |
+
layout process. Each node will try to move along its free axis to
|
150 |
+
reduce the total length of it's edges. This is mainly for the
|
151 |
+
initial layout. If you enable physics, they layout will be determined
|
152 |
+
by the physics. This will greatly speed up the stabilization time
|
153 |
+
"""
|
154 |
+
self.hierarchical.edgeMinimization = status
|
155 |
+
|
156 |
+
class Hierarchical(object):
|
157 |
+
def __getitem__(self, item):
|
158 |
+
return self.__dict__[item]
|
159 |
+
|
160 |
+
def __init__(
|
161 |
+
self,
|
162 |
+
enabled=False,
|
163 |
+
levelSeparation=150,
|
164 |
+
treeSpacing=200,
|
165 |
+
blockShifting=True,
|
166 |
+
edgeMinimization=True,
|
167 |
+
parentCentralization=True,
|
168 |
+
sortMethod="hubsize",
|
169 |
+
):
|
170 |
+
|
171 |
+
self.enabled = enabled
|
172 |
+
self.levelSeparation = levelSeparation
|
173 |
+
self.treeSpacing = treeSpacing
|
174 |
+
self.blockShifting = blockShifting
|
175 |
+
self.edgeMinimization = edgeMinimization
|
176 |
+
self.parentCentralization = parentCentralization
|
177 |
+
self.sortMethod = sortMethod
|
178 |
+
|
179 |
+
|
180 |
+
class Options(object):
|
181 |
+
"""
|
182 |
+
Represents the global options of the network.
|
183 |
+
This object consists of indiviual sub-objects
|
184 |
+
that map to VisJS's modules of:
|
185 |
+
- configure
|
186 |
+
- layout
|
187 |
+
- interaction
|
188 |
+
- physics
|
189 |
+
- edges
|
190 |
+
|
191 |
+
The JSON representation of this object is directly passed
|
192 |
+
in to the VisJS framework.
|
193 |
+
In the future this can be expanded to completely mimic
|
194 |
+
the structure VisJS can expect.
|
195 |
+
"""
|
196 |
+
|
197 |
+
def __repr__(self):
|
198 |
+
return str(self.__dict__)
|
199 |
+
|
200 |
+
def __getitem__(self, item):
|
201 |
+
return self.__dict__[item]
|
202 |
+
|
203 |
+
def __init__(self, layout=None):
|
204 |
+
if layout:
|
205 |
+
self.layout = Layout()
|
206 |
+
self.interaction = Interaction()
|
207 |
+
self.configure = Configure()
|
208 |
+
self.physics = Physics()
|
209 |
+
self.edges = EdgeOptions()
|
210 |
+
|
211 |
+
def set(self, new_options):
|
212 |
+
"""
|
213 |
+
This method should accept a JSON string and replace its internal
|
214 |
+
options structure with the given argument after parsing it.
|
215 |
+
In practice, this method should be called after using the browser
|
216 |
+
to experiment with different physics and layout options, using
|
217 |
+
the generated JSON options structure that is spit out from the
|
218 |
+
front end to serve as input to this method as a string.
|
219 |
+
|
220 |
+
:param new_options: The JSON like string of the options that will
|
221 |
+
override.
|
222 |
+
|
223 |
+
:type new_options: str
|
224 |
+
"""
|
225 |
+
|
226 |
+
options = new_options.replace("\n", "").replace(" ", "")
|
227 |
+
first_bracket = options.find("{")
|
228 |
+
options = options[first_bracket:]
|
229 |
+
options = json.loads(options)
|
230 |
+
return options
|
231 |
+
|
232 |
+
def to_json(self):
|
233 |
+
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
|
pyvis/pyvis/physics.py
ADDED
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
|
3 |
+
|
4 |
+
class Physics(object):
|
5 |
+
|
6 |
+
engine_chosen = False
|
7 |
+
|
8 |
+
def __getitem__(self, item):
|
9 |
+
return self.__dict__[item]
|
10 |
+
|
11 |
+
def __repr__(self):
|
12 |
+
return str(self.__dict__)
|
13 |
+
|
14 |
+
class barnesHut(object):
|
15 |
+
"""
|
16 |
+
BarnesHut is a quadtree based gravity model.
|
17 |
+
This is the fastest, default and recommended.
|
18 |
+
"""
|
19 |
+
|
20 |
+
def __init__(self, params):
|
21 |
+
self.gravitationalConstant = params["gravity"]
|
22 |
+
self.centralGravity = params["central_gravity"]
|
23 |
+
self.springLength = params["spring_length"]
|
24 |
+
self.springConstant = params["spring_strength"]
|
25 |
+
self.damping = params["damping"]
|
26 |
+
self.avoidOverlap = params["overlap"]
|
27 |
+
|
28 |
+
class forceAtlas2Based(object):
|
29 |
+
"""
|
30 |
+
Force Atlas 2 has been develoved by Jacomi et all (2014)
|
31 |
+
for use with Gephi. The force Atlas based solver makes use
|
32 |
+
of some of the equations provided by them and makes use of
|
33 |
+
some of the barnesHut implementation in vis. The Main differences
|
34 |
+
are the central gravity model, which is here distance independent,
|
35 |
+
and repulsion being linear instead of quadratic. Finally, all node
|
36 |
+
masses have a multiplier based on the amount of connected edges
|
37 |
+
plus one.
|
38 |
+
"""
|
39 |
+
|
40 |
+
def __init__(self, params):
|
41 |
+
self.gravitationalConstant = params["gravity"]
|
42 |
+
self.centralGravity = params["central_gravity"]
|
43 |
+
self.springLength = params["spring_length"]
|
44 |
+
self.springConstant = params["spring_strength"]
|
45 |
+
self.damping = params["damping"]
|
46 |
+
self.avoidOverlap = params["overlap"]
|
47 |
+
|
48 |
+
class Repulsion(object):
|
49 |
+
"""
|
50 |
+
The repulsion model assumes nodes have a simplified field
|
51 |
+
around them. Its force lineraly decreases from 1
|
52 |
+
(at 0.5*nodeDistace and smaller) to 0 (at 2*nodeDistance)
|
53 |
+
"""
|
54 |
+
|
55 |
+
def __init__(self, params):
|
56 |
+
self.nodeDistance = params["node_distance"]
|
57 |
+
self.centralGravity = params["central_gravity"]
|
58 |
+
self.springLength = params["spring_length"]
|
59 |
+
self.springConstant = params["spring_strength"]
|
60 |
+
self.damping = params["damping"]
|
61 |
+
|
62 |
+
class hierarchicalRepulsion(object):
|
63 |
+
"""
|
64 |
+
This model is based on the repulsion solver but the levels
|
65 |
+
are taken into accound and the forces
|
66 |
+
are normalized.
|
67 |
+
"""
|
68 |
+
|
69 |
+
def __init__(self, params):
|
70 |
+
self.nodeDistance = params["node_distance"]
|
71 |
+
self.centralGravity = params["central_gravity"]
|
72 |
+
self.springLength = params["spring_length"]
|
73 |
+
self.springConstant = params["spring_strength"]
|
74 |
+
self.damping = params["damping"]
|
75 |
+
|
76 |
+
class Stabilization(object):
|
77 |
+
"""
|
78 |
+
This makes the network stabilized on load using default settings.
|
79 |
+
"""
|
80 |
+
|
81 |
+
def __getitem__(self, item):
|
82 |
+
return self.__dict__[item]
|
83 |
+
|
84 |
+
def __init__(self):
|
85 |
+
self.enabled = True
|
86 |
+
self.iterations = 1000
|
87 |
+
self.updateInterval = 50
|
88 |
+
self.onlyDynamicEdges = False
|
89 |
+
self.fit = True
|
90 |
+
|
91 |
+
def toggle_stabilization(self, status):
|
92 |
+
self.enabled = status
|
93 |
+
|
94 |
+
def __init__(self):
|
95 |
+
self.enabled = True
|
96 |
+
self.stabilization = self.Stabilization()
|
97 |
+
|
98 |
+
def use_barnes_hut(self, params):
|
99 |
+
self.barnesHut = self.barnesHut(params)
|
100 |
+
|
101 |
+
def use_force_atlas_2based(self, params):
|
102 |
+
self.forceAtlas2Based = self.forceAtlas2Based(params)
|
103 |
+
self.solver = "forceAtlas2Based"
|
104 |
+
|
105 |
+
def use_repulsion(self, params):
|
106 |
+
self.repulsion = self.Repulsion(params)
|
107 |
+
self.solver = "repulsion"
|
108 |
+
|
109 |
+
def use_hrepulsion(self, params):
|
110 |
+
self.hierarchicalRepulsion = self.hierarchicalRepulsion(params)
|
111 |
+
self.solver = "hierarchicalRepulsion"
|
112 |
+
|
113 |
+
def toggle_stabilization(self, status):
|
114 |
+
self.stabilization.toggle_stabilization(status)
|
115 |
+
|
116 |
+
def to_json(self):
|
117 |
+
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
|
pyvis/pyvis/source/conf.py
ADDED
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
#
|
3 |
+
# pyvis documentation build configuration file, created by
|
4 |
+
# sphinx-quickstart on Wed Nov 02 20:10:44 2016.
|
5 |
+
#
|
6 |
+
# This file is execfile()d with the current directory set to its
|
7 |
+
# containing dir.
|
8 |
+
#
|
9 |
+
# Note that not all possible configuration values are present in this
|
10 |
+
# autogenerated file.
|
11 |
+
#
|
12 |
+
# All configuration values have a default; values that are commented out
|
13 |
+
# serve to show the default.
|
14 |
+
|
15 |
+
import os
|
16 |
+
import sys
|
17 |
+
|
18 |
+
import sphinx_rtd_theme
|
19 |
+
|
20 |
+
# dynamically read the version
|
21 |
+
exec(open("../_version.py").read())
|
22 |
+
|
23 |
+
# If extensions (or modules to document with autodoc) are in another directory,
|
24 |
+
# add these directories to sys.path here. If the directory is relative to the
|
25 |
+
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
26 |
+
sys.path.insert(0, os.path.abspath("../.."))
|
27 |
+
|
28 |
+
# os.path.abspath('mydir/myfile.txt')
|
29 |
+
|
30 |
+
|
31 |
+
# -- General configuration ------------------------------------------------
|
32 |
+
|
33 |
+
# If your documentation needs a minimal Sphinx version, state it here.
|
34 |
+
# needs_sphinx = '1.0'
|
35 |
+
|
36 |
+
# Add any Sphinx extension module names here, as strings. They can be
|
37 |
+
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
38 |
+
# ones.
|
39 |
+
extensions = [
|
40 |
+
"sphinx.ext.autodoc",
|
41 |
+
"sphinx.ext.intersphinx",
|
42 |
+
"sphinx.ext.ifconfig",
|
43 |
+
"sphinx.ext.viewcode",
|
44 |
+
]
|
45 |
+
|
46 |
+
# Add any paths that contain templates here, relative to this directory.
|
47 |
+
templates_path = ["_templates"]
|
48 |
+
|
49 |
+
# The suffix(es) of source filenames.
|
50 |
+
# You can specify multiple suffix as a list of string:
|
51 |
+
# source_suffix = ['.rst', '.md']
|
52 |
+
source_suffix = ".rst"
|
53 |
+
|
54 |
+
# The encoding of source files.
|
55 |
+
# source_encoding = 'utf-8-sig'
|
56 |
+
|
57 |
+
# The master toctree document.
|
58 |
+
master_doc = "index"
|
59 |
+
|
60 |
+
# General information about the project.
|
61 |
+
project = "pyvis"
|
62 |
+
copyright = "2016-2018, West Health Institute"
|
63 |
+
author = "Giancarlo Perrone"
|
64 |
+
|
65 |
+
# The version info for the project you're documenting, acts as replacement for
|
66 |
+
# |version| and |release|, also used in various other places throughout the
|
67 |
+
# built documents.
|
68 |
+
#
|
69 |
+
# The short X.Y version.
|
70 |
+
version = __version__
|
71 |
+
# The full version, including alpha/beta/rc tags.
|
72 |
+
release = "0.1.3.1"
|
73 |
+
|
74 |
+
# The language for content autogenerated by Sphinx. Refer to documentation
|
75 |
+
# for a list of supported languages.
|
76 |
+
#
|
77 |
+
# This is also used if you do content translation via gettext catalogs.
|
78 |
+
# Usually you set "language" from the command line for these cases.
|
79 |
+
language = None
|
80 |
+
|
81 |
+
# There are two options for replacing |today|: either, you set today to some
|
82 |
+
# non-false value, then it is used:
|
83 |
+
# today = ''
|
84 |
+
# Else, today_fmt is used as the format for a strftime call.
|
85 |
+
# today_fmt = '%B %d, %Y'
|
86 |
+
|
87 |
+
# List of patterns, relative to source directory, that match files and
|
88 |
+
# directories to ignore when looking for source files.
|
89 |
+
# This patterns also effect to html_static_path and html_extra_path
|
90 |
+
exclude_patterns = []
|
91 |
+
|
92 |
+
# The reST default role (used for this markup: `text`) to use for all
|
93 |
+
# documents.
|
94 |
+
# default_role = None
|
95 |
+
|
96 |
+
# If true, '()' will be appended to :func: etc. cross-reference text.
|
97 |
+
# add_function_parentheses = True
|
98 |
+
|
99 |
+
# If true, the current module name will be prepended to all description
|
100 |
+
# unit titles (such as .. function::).
|
101 |
+
# add_module_names = True
|
102 |
+
|
103 |
+
# If true, sectionauthor and moduleauthor directives will be shown in the
|
104 |
+
# output. They are ignored by default.
|
105 |
+
# show_authors = False
|
106 |
+
|
107 |
+
# The name of the Pygments (syntax highlighting) style to use.
|
108 |
+
pygments_style = "sphinx"
|
109 |
+
|
110 |
+
# A list of ignored prefixes for module index sorting.
|
111 |
+
# modindex_common_prefix = []
|
112 |
+
|
113 |
+
# If true, keep warnings as "system message" paragraphs in the built documents.
|
114 |
+
# keep_warnings = False
|
115 |
+
|
116 |
+
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
117 |
+
todo_include_todos = False
|
118 |
+
|
119 |
+
|
120 |
+
# -- Options for HTML output ----------------------------------------------
|
121 |
+
|
122 |
+
# The theme to use for HTML and HTML Help pages. See the documentation for
|
123 |
+
# a list of builtin themes.
|
124 |
+
html_theme = "sphinx_rtd_theme"
|
125 |
+
|
126 |
+
# Theme options are theme-specific and customize the look and feel of a theme
|
127 |
+
# further. For a list of options available for each theme, see the
|
128 |
+
# documentation.
|
129 |
+
# html_theme_options = {}
|
130 |
+
|
131 |
+
# Add any paths that contain custom themes here, relative to this directory.
|
132 |
+
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
133 |
+
|
134 |
+
# The name for this set of Sphinx documents.
|
135 |
+
# "<project> v<release> documentation" by default.
|
136 |
+
# html_title = u'pyvis v0.0.1'
|
137 |
+
|
138 |
+
# A shorter title for the navigation bar. Default is the same as html_title.
|
139 |
+
# html_short_title = None
|
140 |
+
|
141 |
+
# The name of an image file (relative to this directory) to place at the top
|
142 |
+
# of the sidebar.
|
143 |
+
# html_logo = None
|
144 |
+
|
145 |
+
# The name of an image file (relative to this directory) to use as a favicon of
|
146 |
+
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
147 |
+
# pixels large.
|
148 |
+
# html_favicon = None
|
149 |
+
|
150 |
+
# Add any paths that contain custom static files (such as style sheets) here,
|
151 |
+
# relative to this directory. They are copied after the builtin static files,
|
152 |
+
# so a file named "default.css" will overwrite the builtin "default.css".
|
153 |
+
html_static_path = ["_static"]
|
154 |
+
|
155 |
+
# Add any extra paths that contain custom files (such as robots.txt or
|
156 |
+
# .htaccess) here, relative to this directory. These files are copied
|
157 |
+
# directly to the root of the documentation.
|
158 |
+
# html_extra_path = []
|
159 |
+
|
160 |
+
# If not None, a 'Last updated on:' timestamp is inserted at every page
|
161 |
+
# bottom, using the given strftime format.
|
162 |
+
# The empty string is equivalent to '%b %d, %Y'.
|
163 |
+
# html_last_updated_fmt = None
|
164 |
+
|
165 |
+
# If true, SmartyPants will be used to convert quotes and dashes to
|
166 |
+
# typographically correct entities.
|
167 |
+
# html_use_smartypants = True
|
168 |
+
|
169 |
+
# Custom sidebar templates, maps document names to template names.
|
170 |
+
# html_sidebars = {}
|
171 |
+
|
172 |
+
# Additional templates that should be rendered to pages, maps page names to
|
173 |
+
# template names.
|
174 |
+
# html_additional_pages = {}
|
175 |
+
|
176 |
+
# If false, no module index is generated.
|
177 |
+
# html_domain_indices = True
|
178 |
+
|
179 |
+
# If false, no index is generated.
|
180 |
+
# html_use_index = True
|
181 |
+
|
182 |
+
# If true, the index is split into individual pages for each letter.
|
183 |
+
# html_split_index = False
|
184 |
+
|
185 |
+
# If true, links to the reST sources are added to the pages.
|
186 |
+
# html_show_sourcelink = True
|
187 |
+
|
188 |
+
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
189 |
+
# html_show_sphinx = True
|
190 |
+
|
191 |
+
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
192 |
+
# html_show_copyright = True
|
193 |
+
|
194 |
+
# If true, an OpenSearch description file will be output, and all pages will
|
195 |
+
# contain a <link> tag referring to it. The value of this option must be the
|
196 |
+
# base URL from which the finished HTML is served.
|
197 |
+
# html_use_opensearch = ''
|
198 |
+
|
199 |
+
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
200 |
+
# html_file_suffix = None
|
201 |
+
|
202 |
+
# Language to be used for generating the HTML full-text search index.
|
203 |
+
# Sphinx supports the following languages:
|
204 |
+
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
205 |
+
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
|
206 |
+
# html_search_language = 'en'
|
207 |
+
|
208 |
+
# A dictionary with options for the search language support, empty by default.
|
209 |
+
# 'ja' uses this config value.
|
210 |
+
# 'zh' user can custom change `jieba` dictionary path.
|
211 |
+
# html_search_options = {'type': 'default'}
|
212 |
+
|
213 |
+
# The name of a javascript file (relative to the configuration directory) that
|
214 |
+
# implements a search results scorer. If empty, the default will be used.
|
215 |
+
# html_search_scorer = 'scorer.js'
|
216 |
+
|
217 |
+
# Output file base name for HTML help builder.
|
218 |
+
htmlhelp_basename = "pyvisdoc"
|
219 |
+
|
220 |
+
# -- Options for LaTeX output ---------------------------------------------
|
221 |
+
|
222 |
+
latex_elements = {
|
223 |
+
# The paper size ('letterpaper' or 'a4paper').
|
224 |
+
#'papersize': 'letterpaper',
|
225 |
+
# The font size ('10pt', '11pt' or '12pt').
|
226 |
+
#'pointsize': '10pt',
|
227 |
+
# Additional stuff for the LaTeX preamble.
|
228 |
+
#'preamble': '',
|
229 |
+
# Latex figure (float) alignment
|
230 |
+
#'figure_align': 'htbp',
|
231 |
+
}
|
232 |
+
|
233 |
+
# Grouping the document tree into LaTeX files. List of tuples
|
234 |
+
# (source start file, target name, title,
|
235 |
+
# author, documentclass [howto, manual, or own class]).
|
236 |
+
latex_documents = [
|
237 |
+
(
|
238 |
+
master_doc,
|
239 |
+
"pyvis.tex",
|
240 |
+
"pyvis Documentation",
|
241 |
+
"Giancarlo Perrone, Shashank Soni",
|
242 |
+
"manual",
|
243 |
+
),
|
244 |
+
]
|
245 |
+
|
246 |
+
# The name of an image file (relative to this directory) to place at the top of
|
247 |
+
# the title page.
|
248 |
+
# latex_logo = None
|
249 |
+
|
250 |
+
# For "manual" documents, if this is true, then toplevel headings are parts,
|
251 |
+
# not chapters.
|
252 |
+
# latex_use_parts = False
|
253 |
+
|
254 |
+
# If true, show page references after internal links.
|
255 |
+
# latex_show_pagerefs = False
|
256 |
+
|
257 |
+
# If true, show URL addresses after external links.
|
258 |
+
# latex_show_urls = False
|
259 |
+
|
260 |
+
# Documents to append as an appendix to all manuals.
|
261 |
+
# latex_appendices = []
|
262 |
+
|
263 |
+
# If false, no module index is generated.
|
264 |
+
# latex_domain_indices = True
|
265 |
+
|
266 |
+
|
267 |
+
# -- Options for manual page output ---------------------------------------
|
268 |
+
|
269 |
+
# One entry per manual page. List of tuples
|
270 |
+
# (source start file, name, description, authors, manual section).
|
271 |
+
man_pages = [(master_doc, "pyvis", "pyvis Documentation", [author], 1)]
|
272 |
+
|
273 |
+
# If true, show URL addresses after external links.
|
274 |
+
# man_show_urls = False
|
275 |
+
|
276 |
+
|
277 |
+
# -- Options for Texinfo output -------------------------------------------
|
278 |
+
|
279 |
+
# Grouping the document tree into Texinfo files. List of tuples
|
280 |
+
# (source start file, target name, title, author,
|
281 |
+
# dir menu entry, description, category)
|
282 |
+
texinfo_documents = [
|
283 |
+
(
|
284 |
+
master_doc,
|
285 |
+
"pyvis",
|
286 |
+
"pyvis Documentation",
|
287 |
+
author,
|
288 |
+
"pyvis",
|
289 |
+
"One line description of project.",
|
290 |
+
"Miscellaneous",
|
291 |
+
),
|
292 |
+
]
|
293 |
+
|
294 |
+
# Documents to append as an appendix to all manuals.
|
295 |
+
# texinfo_appendices = []
|
296 |
+
|
297 |
+
# If false, no module index is generated.
|
298 |
+
# texinfo_domain_indices = True
|
299 |
+
|
300 |
+
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
301 |
+
# texinfo_show_urls = 'footnote'
|
302 |
+
|
303 |
+
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
304 |
+
# texinfo_no_detailmenu = False
|
305 |
+
|
306 |
+
|
307 |
+
# Example configuration for intersphinx: refer to the Python standard library.
|
308 |
+
intersphinx_mapping = {"https://docs.python.org/": None}
|
pyvis/pyvis/source/documentation.rst
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
=============
|
2 |
+
Documentation
|
3 |
+
=============
|
4 |
+
|
5 |
+
.. automodule:: pyvis.network
|
6 |
+
:members:
|
7 |
+
|
8 |
+
.. automodule:: pyvis.options
|
9 |
+
:members:
|
pyvis/pyvis/source/index.rst
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
.. image:: net.png
|
3 |
+
:width: 30%
|
4 |
+
.. image:: net2.png
|
5 |
+
:width: 30%
|
6 |
+
.. image:: net3.png
|
7 |
+
:width: 30%
|
8 |
+
|
9 |
+
Interactive network visualizations
|
10 |
+
==================================
|
11 |
+
.. image:: tut.gif
|
12 |
+
|
13 |
+
Contents:
|
14 |
+
=========
|
15 |
+
.. toctree::
|
16 |
+
:maxdepth: 2
|
17 |
+
|
18 |
+
install
|
19 |
+
introduction
|
20 |
+
tutorial
|
21 |
+
license
|
22 |
+
documentation
|
23 |
+
|
24 |
+
|
25 |
+
|
26 |
+
|
27 |
+
Indices and tables
|
28 |
+
------------------
|
29 |
+
|
30 |
+
* :ref:`genindex`
|
31 |
+
* :ref:`modindex`
|
32 |
+
* :ref:`search`
|
33 |
+
* :ref:`glossary`
|
pyvis/pyvis/source/install.rst
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
============
|
2 |
+
Installation
|
3 |
+
============
|
4 |
+
|
5 |
+
----------------
|
6 |
+
Install with pip
|
7 |
+
----------------
|
8 |
+
|
9 |
+
.. code-block:: bash
|
10 |
+
|
11 |
+
$ pip install pyvis
|
12 |
+
|
13 |
+
Or you can download an archive of the project here_. To install, unpack it
|
14 |
+
and run the following in the top-level directory:
|
15 |
+
|
16 |
+
.. code-block:: bash
|
17 |
+
|
18 |
+
$ python setup.py install
|
19 |
+
|
20 |
+
.. _here: https://github.com/WestHealth/pyvis/
|
pyvis/pyvis/source/introduction.rst
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
============
|
2 |
+
Introduction
|
3 |
+
============
|
4 |
+
|
5 |
+
The goal of this project is to build a python based approach to constructing and visualizing
|
6 |
+
network graphs in the same space. A pyvis network can be customized on a per node or per edge
|
7 |
+
basis. Nodes can be given colors, sizes, labels, and other metadata. Each graph can be interacted
|
8 |
+
with, allowing the dragging, hovering, and selection of nodes and edges. Each graph's layout
|
9 |
+
algorithm can be tweaked as well to allow experimentation with rendering of larger graphs.
|
10 |
+
|
11 |
+
Pyvis is built around the amazing VisJS_ library.
|
12 |
+
|
13 |
+
.. _VisJS: https://visjs.github.io/vis-network/examples/
|
pyvis/pyvis/source/jup.png
ADDED
![]() |
pyvis/pyvis/source/license.rst
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
============
|
2 |
+
License
|
3 |
+
============
|
4 |
+
|
5 |
+
Pyvis is distributed with the BSD 3 Clause license.
|
6 |
+
|
7 |
+
Copyright (c) 2018, West Health Institute
|
8 |
+
All rights reserved.
|
9 |
+
|
10 |
+
Redistribution and use in source and binary forms, with or without modification,
|
11 |
+
are permitted provided that the following conditions are met:
|
12 |
+
|
13 |
+
- Redistributions of source code must retain the above copyright notice,
|
14 |
+
this list of conditions and the following disclaimer.
|
15 |
+
|
16 |
+
- Redistributions in binary form must reproduce the above copyright notice,
|
17 |
+
this list of conditions and the following disclaimer in the documentation
|
18 |
+
and/or other materials provided with the distribution.
|
19 |
+
|
20 |
+
- Neither the name of West Health Institute nor the names of its contributors may
|
21 |
+
be used to endorse or promote products derived from this software without
|
22 |
+
specific prior written permission.
|
23 |
+
|
24 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
25 |
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
26 |
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
27 |
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
28 |
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
29 |
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
30 |
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
31 |
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
32 |
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
33 |
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
pyvis/pyvis/source/net.png
ADDED
![]() |
pyvis/pyvis/source/net2.png
ADDED
![]() |
pyvis/pyvis/source/net3.png
ADDED
![]() |
pyvis/pyvis/source/rednode.png
ADDED
![]() |
pyvis/pyvis/source/tutorial.rst
ADDED
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
============
|
3 |
+
Tutorial
|
4 |
+
============
|
5 |
+
The pyvis library is meant for quick generation of visual network graphs
|
6 |
+
with minimal python code. It is designed as a wrapper around the popular
|
7 |
+
Javascript visJS library found at this link_.
|
8 |
+
|
9 |
+
.. _link: https://visjs.github.io/vis-network/examples/
|
10 |
+
|
11 |
+
|
12 |
+
Getting started
|
13 |
+
---------------
|
14 |
+
All networks must be instantiated as a ``Network`` class instance
|
15 |
+
|
16 |
+
>>> from pyvis.network import Network
|
17 |
+
>>> net = Network()
|
18 |
+
|
19 |
+
Add nodes to the network
|
20 |
+
------------------------
|
21 |
+
>>> net.add_node(1, label="Node 1") # node id = 1 and label = Node 1
|
22 |
+
>>> net.add_node(2) # node id and label = 2
|
23 |
+
|
24 |
+
Here, the first parameter to the add_node method is the desired ID to give the
|
25 |
+
Node. This can be a string or a numeric. The label argument is the string that
|
26 |
+
will be visibly attached to the node in the final visualization. If no label
|
27 |
+
argument is specified then the node id will be used as a label.
|
28 |
+
|
29 |
+
.. note:: The ``ID`` parameter must be unique
|
30 |
+
|
31 |
+
You can also add a list of nodes
|
32 |
+
|
33 |
+
>>> nodes = ["a", "b", "c", "d"]
|
34 |
+
>>> net.add_nodes(nodes) # node ids and labels = ["a", "b", "c", "d"]
|
35 |
+
>>> net.add_nodes("hello") # node ids and labels = ["h", "e", "l", "o"]
|
36 |
+
|
37 |
+
.. note:: :meth:`network.Network.add_nodes` accepts any iterable as long as the contents are strings or numerics
|
38 |
+
|
39 |
+
Node properties
|
40 |
+
---------------
|
41 |
+
A call to :meth:`add_node` supports various node properties that can be set
|
42 |
+
individually. All of these properties can be found here_, courtesy of VisJS_.
|
43 |
+
For the direct Python translation of these attributes, reference the
|
44 |
+
:meth:`network.Network.add_node` docs.
|
45 |
+
|
46 |
+
.. _here: https://visjs.github.io/vis-network/docs/network/nodes.html
|
47 |
+
.. _VisJS: https://visjs.github.io/vis-network/docs/network/
|
48 |
+
|
49 |
+
.. note:: Through no fault of pyvis, some of the attributes in the VisJS_ documentation do not
|
50 |
+
work as expected, or at all. Pyvis can translate into the JavaScript
|
51 |
+
elements for VisJS_ but after that it's up to VisJS_!
|
52 |
+
|
53 |
+
Indexing a Node
|
54 |
+
---------------
|
55 |
+
Use the :meth:`get_node` method to index a node by its ID:
|
56 |
+
|
57 |
+
>>> net.add_nodes(["a", "b", "c"])
|
58 |
+
>>> net.get_node("c")
|
59 |
+
>>> {'id': 'c', 'label': 'c', 'shape': 'dot'}
|
60 |
+
|
61 |
+
|
62 |
+
Adding list of nodes with properties
|
63 |
+
------------------------------------
|
64 |
+
When using the :meth:`network.Network.add_nodes` method optional keyword arguments can be
|
65 |
+
passed in to add properties to these nodes as well. The valid properties in this case are
|
66 |
+
|
67 |
+
>>> ['size', 'value', 'title', 'x', 'y', 'label', 'color']
|
68 |
+
|
69 |
+
Each of these keyword args must be the same length as the nodes parameter to the method.
|
70 |
+
|
71 |
+
Example:
|
72 |
+
|
73 |
+
>>> g = Network()
|
74 |
+
>>> g.add_nodes([1,2,3], value=[10, 100, 400],
|
75 |
+
title=['I am node 1', 'node 2 here', 'and im node 3'],
|
76 |
+
x=[21.4, 54.2, 11.2],
|
77 |
+
y=[100.2, 23.54, 32.1],
|
78 |
+
label=['NODE 1', 'NODE 2', 'NODE 3'],
|
79 |
+
color=['#00ff1e', '#162347', '#dd4b39'])
|
80 |
+
|
81 |
+
.. raw:: html
|
82 |
+
:file: mulnodes.html
|
83 |
+
|
84 |
+
.. note:: If you mouse over each node you will see that the ``title`` of a node
|
85 |
+
attribute is responsible for rendering data on mouse hover. You can add ``HTML``
|
86 |
+
in your ``title`` string and it will be rendered as such.
|
87 |
+
|
88 |
+
.. note:: The ``color`` attribute can also be a plain HTML ``color`` like red or blue. You can also
|
89 |
+
specify the full ``rgba`` specification if needed. The VisJS_ documentation has more
|
90 |
+
details.
|
91 |
+
|
92 |
+
Detailed optional argument documentation for nodes are in the
|
93 |
+
:meth:`network.Network.add_node` method documentation.
|
94 |
+
|
95 |
+
Edges
|
96 |
+
-----
|
97 |
+
|
98 |
+
Assuming the network's nodes exist, the edges can then be added according to node id's
|
99 |
+
|
100 |
+
>>> net.add_node(0, label='a')
|
101 |
+
>>> net.add_node(1, label='b')
|
102 |
+
>>> net.add_edge(0, 1)
|
103 |
+
|
104 |
+
Edges can contain a ``weight`` attribute as well
|
105 |
+
|
106 |
+
>>> net.add_edge(0, 1, weight=.87)
|
107 |
+
|
108 |
+
Edges can be customized and documentation on options can be found at
|
109 |
+
:meth:`network.Network.add_edge` method documentation, or by referencing the
|
110 |
+
original VisJS edge_ module docs.
|
111 |
+
|
112 |
+
.. _edge: https://visjs.github.io/vis-network/docs/network/edges.html
|
113 |
+
|
114 |
+
`Networkx <https://networkx.github.io/>`_ integration
|
115 |
+
------------------------------------------------------
|
116 |
+
|
117 |
+
An easy way to visualize and construct pyvis networks is to use `Networkx <https://networkx.github.io>`_
|
118 |
+
and use pyvis's built-in networkx helper method to translate the graph. Note that the
|
119 |
+
Networkx node properties with the same names as those consumed by pyvis (e.g., ``title``) are
|
120 |
+
translated directly to the correspondingly-named pyvis node attributes.
|
121 |
+
|
122 |
+
>>> from pyvis.network import Network
|
123 |
+
>>> import networkx as nx
|
124 |
+
>>> nx_graph = nx.cycle_graph(10)
|
125 |
+
>>> nx_graph.nodes[1]['title'] = 'Number 1'
|
126 |
+
>>> nx_graph.nodes[1]['group'] = 1
|
127 |
+
>>> nx_graph.nodes[3]['title'] = 'I belong to a different group!'
|
128 |
+
>>> nx_graph.nodes[3]['group'] = 10
|
129 |
+
>>> nx_graph.add_node(20, size=20, title='couple', group=2)
|
130 |
+
>>> nx_graph.add_node(21, size=15, title='couple', group=2)
|
131 |
+
>>> nx_graph.add_edge(20, 21, weight=5)
|
132 |
+
>>> nx_graph.add_node(25, size=25, label='lonely', title='lonely node', group=3)
|
133 |
+
>>> nt = Network('500px', '500px')
|
134 |
+
# populates the nodes and edges data structures
|
135 |
+
>>> nt.from_nx(nx_graph)
|
136 |
+
>>> nt.show('nx.html')
|
137 |
+
|
138 |
+
.. raw:: html
|
139 |
+
:file: nx.html
|
140 |
+
|
141 |
+
|
142 |
+
Visualization
|
143 |
+
-------------
|
144 |
+
|
145 |
+
The displaying of a graph is achieved by a single method call on
|
146 |
+
:meth:`network.Network.show()` after the underlying network is constructed.
|
147 |
+
The interactive visualization is presented as a static HTML file.
|
148 |
+
|
149 |
+
>>> net.toggle_physics(True)
|
150 |
+
>>> net.show('mygraph.html')
|
151 |
+
|
152 |
+
.. note:: Triggering the :meth:`toggle_physics` method allows for more fluid graph interactions
|
153 |
+
|
154 |
+
Example: Visualizing a Game of Thrones character network
|
155 |
+
--------------------------------------------------------
|
156 |
+
|
157 |
+
The following code block is a minimal example of the capabilities of pyvis.
|
158 |
+
|
159 |
+
.. code-block:: python
|
160 |
+
|
161 |
+
from pyvis.network import Network
|
162 |
+
import pandas as pd
|
163 |
+
|
164 |
+
got_net = Network(height='750px', width='100%', bgcolor='#222222', font_color='white')
|
165 |
+
|
166 |
+
# set the physics layout of the network
|
167 |
+
got_net.barnes_hut()
|
168 |
+
got_data = pd.read_csv('https://www.macalester.edu/~abeverid/data/stormofswords.csv')
|
169 |
+
|
170 |
+
sources = got_data['Source']
|
171 |
+
targets = got_data['Target']
|
172 |
+
weights = got_data['Weight']
|
173 |
+
|
174 |
+
edge_data = zip(sources, targets, weights)
|
175 |
+
|
176 |
+
for e in edge_data:
|
177 |
+
src = e[0]
|
178 |
+
dst = e[1]
|
179 |
+
w = e[2]
|
180 |
+
|
181 |
+
got_net.add_node(src, src, title=src)
|
182 |
+
got_net.add_node(dst, dst, title=dst)
|
183 |
+
got_net.add_edge(src, dst, value=w)
|
184 |
+
|
185 |
+
neighbor_map = got_net.get_adj_list()
|
186 |
+
|
187 |
+
# add neighbor data to node hover data
|
188 |
+
for node in got_net.nodes:
|
189 |
+
node['title'] += ' Neighbors:<br>' + '<br>'.join(neighbor_map[node['id']])
|
190 |
+
node['value'] = len(neighbor_map[node['id']])
|
191 |
+
|
192 |
+
got_net.show('gameofthrones.html')
|
193 |
+
|
194 |
+
|
195 |
+
If you want to try out the above code, the csv data source can be `downloaded <https://www.macalester.edu/~abeverid/data/stormofswords.csv>`_
|
196 |
+
|
197 |
+
.. note:: The ``title`` attribute of each node is responsible for rendering data on node hover.
|
198 |
+
|
199 |
+
.. raw:: html
|
200 |
+
:file: gameofthrones.html
|
201 |
+
|
202 |
+
Using the configuration UI to dynamically tweak ``Network`` settings
|
203 |
+
----------------------------------------------------------------
|
204 |
+
You also have the option of supplying your visualization with a UI used to
|
205 |
+
dynamically alter some of the settings pertaining to your network. This could
|
206 |
+
be useful for finding the most optimal parameters to your graph's physics and
|
207 |
+
layout function.
|
208 |
+
|
209 |
+
>>> net.show_buttons(filter_=['physics'])
|
210 |
+
|
211 |
+
.. image:: buttons.gif
|
212 |
+
|
213 |
+
.. note:: You can copy/paste the output from the `generate options` button in the above UI
|
214 |
+
into :meth:`network.Network.set_options` to finalize your results from experimentation
|
215 |
+
with the settings.
|
216 |
+
|
217 |
+
.. image:: set_options_ex.gif
|
218 |
+
|
219 |
+
Using pyvis within `Jupyter <https://jupyter.org>`_ notebook
|
220 |
+
------------------------------------------------------------
|
221 |
+
|
222 |
+
Pyvis supports `Jupyter <https://jupyter.org>`_ notebook embedding through the
|
223 |
+
use of the :meth:`network.Network` constructor. The network instance must be
|
224 |
+
"prepped" during instantiation by supplying the `notebook=True` kwarg.
|
225 |
+
Example:
|
226 |
+
|
227 |
+
.. image:: jup.png
|
pyvis/pyvis/tests/__init__.py
ADDED
File without changes
|
pyvis/pyvis/tests/test_graph.py
ADDED
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import unittest
|
3 |
+
|
4 |
+
from ..network import Network
|
5 |
+
|
6 |
+
|
7 |
+
class NodeTestCase(unittest.TestCase):
|
8 |
+
def setUp(self):
|
9 |
+
self.g = Network()
|
10 |
+
|
11 |
+
def test_one_node_g(self):
|
12 |
+
self.g.add_node(0, 0)
|
13 |
+
self.assertEqual(0, self.g.nodes[0]["id"])
|
14 |
+
self.assertEqual(0, self.g.nodes[0]["label"])
|
15 |
+
self.assertTrue(0 in self.g.get_nodes())
|
16 |
+
|
17 |
+
def test_one_conn(self):
|
18 |
+
self.g.add_nodes([0, 1])
|
19 |
+
self.assertTrue(1 in self.g.get_nodes())
|
20 |
+
self.assertTrue(0 in self.g.get_nodes())
|
21 |
+
self.g.add_edge(0, 1)
|
22 |
+
self.assertTrue(
|
23 |
+
self.g.get_edges()[0]["from"] == 0 and self.g.get_edges()[0]["to"] == 1
|
24 |
+
)
|
25 |
+
|
26 |
+
def test_no_dup_edges(self):
|
27 |
+
self.g.add_nodes([0, 1])
|
28 |
+
self.g.add_edge(0, 1)
|
29 |
+
self.assertTrue(len(self.g.get_edges()) == 1)
|
30 |
+
self.g.add_edge(1, 0)
|
31 |
+
self.assertTrue(len(self.g.get_edges()) == 1)
|
32 |
+
|
33 |
+
def test_no_dup_nodes(self):
|
34 |
+
self.g.add_node(100, 100)
|
35 |
+
self.g.add_node(100, 100)
|
36 |
+
self.assertTrue(len(self.g.nodes) == 1)
|
37 |
+
self.g.add_node(100, "n101")
|
38 |
+
self.assertTrue(len(self.g.nodes) == 1)
|
39 |
+
|
40 |
+
def test_node_labels(self):
|
41 |
+
self.g.add_node(1, "n2")
|
42 |
+
self.g.add_node(2, "n3")
|
43 |
+
self.g.add_node(3, "n4")
|
44 |
+
self.assertEqual(
|
45 |
+
set(map(lambda s: s["label"], self.g.nodes)), set(["n2", "n3", "n4"])
|
46 |
+
)
|
47 |
+
|
48 |
+
def test_node_titles(self):
|
49 |
+
self.g.add_nodes(range(10))
|
50 |
+
|
51 |
+
for n in self.g.nodes:
|
52 |
+
n["title"] = "node #" + str(n["id"])
|
53 |
+
|
54 |
+
ref = ["node #" + str(i) for i in range(10)]
|
55 |
+
self.assertEqual(set(map(lambda s: s["title"], self.g.nodes)), set(ref))
|
56 |
+
|
57 |
+
def test_node_sizes(self):
|
58 |
+
self.g.add_nodes(range(10))
|
59 |
+
|
60 |
+
for n in self.g.nodes:
|
61 |
+
n["size"] = n["id"] * 2
|
62 |
+
|
63 |
+
ref = [i * 2 for i in range(10)]
|
64 |
+
self.assertEqual(set(map(lambda s: s["size"], self.g.nodes)), set(ref))
|
65 |
+
|
66 |
+
def test_adding_nodes(self):
|
67 |
+
g = self.g
|
68 |
+
g.add_nodes(range(5))
|
69 |
+
self.assertTrue(g.get_nodes(), range(5))
|
70 |
+
|
71 |
+
def test_adding_nodes_with_props(self):
|
72 |
+
g = self.g
|
73 |
+
g.add_nodes(
|
74 |
+
range(4),
|
75 |
+
size=range(4),
|
76 |
+
color=range(4),
|
77 |
+
title=range(4),
|
78 |
+
label=range(4),
|
79 |
+
x=range(4),
|
80 |
+
y=range(4),
|
81 |
+
)
|
82 |
+
for i, n in enumerate(g.nodes):
|
83 |
+
self.assertEqual(n["size"], i)
|
84 |
+
self.assertEqual(n["color"], i)
|
85 |
+
self.assertEqual(n["title"], i)
|
86 |
+
self.assertEqual(n["label"], i)
|
87 |
+
self.assertEqual(n["x"], i)
|
88 |
+
self.assertEqual(n["y"], i)
|
89 |
+
|
90 |
+
def test_labels(self):
|
91 |
+
g = self.g
|
92 |
+
g.add_node(0)
|
93 |
+
self.assertTrue(g.nodes[0]["label"] == 0)
|
94 |
+
g.add_node(50, label="NODE 50")
|
95 |
+
self.assertTrue(g.nodes[1]["label"] == "NODE 50")
|
96 |
+
|
97 |
+
def test_nodes_with_stringids(self):
|
98 |
+
g = self.g
|
99 |
+
g.add_nodes(["n1", "n2", "n3"])
|
100 |
+
self.assertTrue(g.node_ids == ["n1", "n2", "n3"])
|
101 |
+
|
102 |
+
def test_adj_list(self):
|
103 |
+
g = self.g
|
104 |
+
g.add_nodes(range(4))
|
105 |
+
g.add_edges([(0, 1, 1), (0, 2, 1), (0, 3, 1), (1, 3, 1)])
|
106 |
+
alist = g.get_adj_list()
|
107 |
+
self.assertEqual(len(alist), g.num_nodes())
|
108 |
+
self.assertEqual(alist[0], set([1, 2, 3]))
|
109 |
+
self.assertEqual(alist[1], set([0, 3]))
|
110 |
+
|
111 |
+
def test_neighbors(self):
|
112 |
+
g = self.g
|
113 |
+
g.add_nodes(range(5))
|
114 |
+
g.add_edge(0, 1)
|
115 |
+
g.add_edge(0, 2)
|
116 |
+
g.add_edge(1, 3)
|
117 |
+
g.add_edge(1, 4)
|
118 |
+
self.assertEqual(g.neighbors(0), set([1, 2]))
|
119 |
+
|
120 |
+
def test_length(self):
|
121 |
+
g = self.g
|
122 |
+
g.add_node(0)
|
123 |
+
self.assertEqual(g.num_nodes(), 1)
|
124 |
+
|
125 |
+
def test_get_network_data(self):
|
126 |
+
self.assertEqual(len(self.g.get_network_data()), 6)
|
127 |
+
|
128 |
+
|
129 |
+
class EdgeTestCase(unittest.TestCase):
|
130 |
+
def setUp(self):
|
131 |
+
self.g = Network()
|
132 |
+
self.g.add_nodes([0, 1, 2, 3])
|
133 |
+
|
134 |
+
def test_non_existent_edge(self):
|
135 |
+
self.assertRaises(AssertionError, self.g.add_edge, 5, 1)
|
136 |
+
self.assertRaises(AssertionError, self.g.add_edge, "node1", "node2")
|
137 |
+
|
138 |
+
def test_no_edge_length(self):
|
139 |
+
self.assertTrue(self.g.num_nodes() == 4)
|
140 |
+
self.assertTrue(self.g.num_edges() == 0)
|
141 |
+
|
142 |
+
def test_add_one_edge(self):
|
143 |
+
self.g.add_edge(0, 1)
|
144 |
+
self.assertTrue(self.g.num_edges() == 1)
|
145 |
+
self.assertTrue({"from": 0, "to": 1} in self.g.edges)
|
146 |
+
|
147 |
+
def test_add_two_edges_no_dups(self):
|
148 |
+
self.g.add_edge(0, 1)
|
149 |
+
self.g.add_edge(0, 1)
|
150 |
+
self.assertTrue(self.g.num_edges() == 1)
|
151 |
+
self.g.add_edge(1, 2)
|
152 |
+
self.assertTrue(self.g.num_edges() == 2)
|
153 |
+
self.assertEqual([{"from": 0, "to": 1}, {"from": 1, "to": 2}], self.g.edges)
|
154 |
+
|
155 |
+
def test_add_edges_no_weights(self):
|
156 |
+
self.g.add_edges([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)])
|
157 |
+
self.assertEqual(self.g.num_edges(), 6)
|
158 |
+
self.assertEqual(self.g.neighbors(0), set([1, 2, 3]))
|
159 |
+
self.assertEqual(self.g.neighbors(1), set([0, 2, 3]))
|
160 |
+
self.assertEqual(self.g.neighbors(2), set([0, 1, 3]))
|
161 |
+
self.assertEqual(self.g.neighbors(3), set([0, 1, 2]))
|
162 |
+
self.assertTrue("weight" not in [es for es in self.g.edges])
|
163 |
+
|
164 |
+
def test_add_edges_weights(self):
|
165 |
+
self.g.add_edges(
|
166 |
+
[(0, 1, 1), (0, 2, 2), (0, 3, 3), (1, 2, 4), (1, 3, 5), (2, 3, 6)]
|
167 |
+
)
|
168 |
+
self.assertEqual(self.g.num_edges(), 6)
|
169 |
+
self.assertEqual(self.g.neighbors(0), set([1, 2, 3]))
|
170 |
+
self.assertEqual(self.g.neighbors(1), set([0, 2, 3]))
|
171 |
+
self.assertEqual(self.g.neighbors(2), set([0, 1, 3]))
|
172 |
+
self.assertEqual(self.g.neighbors(3), set([0, 1, 2]))
|
173 |
+
for edges in self.g.edges:
|
174 |
+
self.assertTrue("width" in edges)
|
175 |
+
self.assertEqual(
|
176 |
+
list([1, 2, 3, 4, 5, 6]), list(map(lambda x: x["width"], self.g.edges))
|
177 |
+
)
|
178 |
+
|
179 |
+
def test_add_edges_mixed_weights(self):
|
180 |
+
self.g.add_edges([(0, 1, 1), (0, 2), (0, 3, 3), (1, 2), (1, 3, 5), (2, 3)])
|
181 |
+
self.assertEqual(self.g.num_edges(), 6)
|
182 |
+
self.assertEqual(self.g.neighbors(0), set([1, 2, 3]))
|
183 |
+
self.assertEqual(self.g.neighbors(1), set([0, 2, 3]))
|
184 |
+
self.assertEqual(self.g.neighbors(2), set([0, 1, 3]))
|
185 |
+
self.assertEqual(self.g.neighbors(3), set([0, 1, 2]))
|
186 |
+
self.assertEqual(
|
187 |
+
list([1, None, 3, None, 5, None]),
|
188 |
+
list(map(lambda x: x.get("width", None), self.g.edges)),
|
189 |
+
)
|
190 |
+
|
191 |
+
def test_add_edge_directed(self):
|
192 |
+
self.g.directed = True
|
193 |
+
self.g.add_edge(0, 1)
|
194 |
+
self.assertTrue(self.g.edges)
|
195 |
+
for e in self.g.edges:
|
196 |
+
self.assertTrue(e["arrows"] == "to")
|
197 |
+
|
198 |
+
|
199 |
+
class UtilsTestCase(unittest.TestCase):
|
200 |
+
def setUp(self):
|
201 |
+
self.g = Network()
|
202 |
+
self.g.add_nodes([0, 1, 2, 3])
|
203 |
+
|
204 |
+
def test_html_naming(self):
|
205 |
+
self.assertRaises(AssertionError, self.g.write_html, "4nodes.htl")
|
206 |
+
self.assertRaises(AssertionError, self.g.write_html, "4nodes.hltm")
|
207 |
+
self.assertRaises(AssertionError, self.g.write_html, "4nodes. htl")
|
208 |
+
self.g.write_html("4nodes.html")
|
209 |
+
self.assertTrue("4nodes.html" in os.listdir("."))
|
210 |
+
os.remove("4nodes.html")
|
211 |
+
|
212 |
+
|
213 |
+
class PhysicsTestCase(unittest.TestCase):
|
214 |
+
def setUp(self):
|
215 |
+
self.g = Network()
|
216 |
+
self.g.add_nodes([0, 1, 2, 3])
|
217 |
+
|
218 |
+
def test_barnes_hut(self):
|
219 |
+
self.g.barnes_hut()
|
220 |
+
self.assertTrue("barnesHut" in vars(self.g.options.physics))
|
221 |
+
|
222 |
+
def test_repulsion(self):
|
223 |
+
self.g.repulsion()
|
224 |
+
self.assertTrue("repulsion" in vars(self.g.options.physics))
|
225 |
+
|
226 |
+
def test_hrepulsion(self):
|
227 |
+
self.g.hrepulsion()
|
228 |
+
self.assertTrue("hierarchicalRepulsion" in vars(self.g.options.physics))
|
229 |
+
|
230 |
+
def test_force_atlas_2based(self):
|
231 |
+
self.g.force_atlas_2based()
|
232 |
+
self.assertTrue("forceAtlas2Based" in vars(self.g.options.physics))
|
233 |
+
|
234 |
+
def test_toggle_physics(self):
|
235 |
+
self.assertTrue(self.g.options.physics["enabled"])
|
236 |
+
self.g.toggle_physics(False)
|
237 |
+
self.assertFalse(self.g.options.physics["enabled"])
|
238 |
+
|
239 |
+
def test_stabilization(self):
|
240 |
+
self.g.toggle_stabilization(True)
|
241 |
+
self.assertTrue(self.g.options.physics.stabilization["enabled"])
|
242 |
+
self.g.toggle_stabilization(False)
|
243 |
+
self.assertFalse(self.g.options.physics.stabilization["enabled"])
|
244 |
+
|
245 |
+
|
246 |
+
class InteractionTestCase(unittest.TestCase):
|
247 |
+
def setUp(self):
|
248 |
+
self.g = Network()
|
249 |
+
self.g.add_nodes([0, 1, 2, 3])
|
250 |
+
|
251 |
+
def test_toggle_drag_nodes(self):
|
252 |
+
self.assertTrue(self.g.options.interaction["dragNodes"])
|
253 |
+
self.g.toggle_drag_nodes(False)
|
254 |
+
self.assertFalse(self.g.options.interaction["dragNodes"])
|
255 |
+
|
256 |
+
def test_toggle_hide_edges_on_drag(self):
|
257 |
+
self.assertFalse(self.g.options.interaction["hideEdgesOnDrag"])
|
258 |
+
self.g.toggle_hide_edges_on_drag(True)
|
259 |
+
self.assertTrue(self.g.options.interaction["hideEdgesOnDrag"])
|
260 |
+
|
261 |
+
def test_toggle_hide_nodes_on_drag(self):
|
262 |
+
self.assertFalse(self.g.options.interaction["hideNodesOnDrag"])
|
263 |
+
self.g.toggle_hide_nodes_on_drag(True)
|
264 |
+
self.assertTrue(self.g.options.interaction["hideNodesOnDrag"])
|
265 |
+
|
266 |
+
|
267 |
+
class ConfigureTestCase(unittest.TestCase):
|
268 |
+
def setUp(self):
|
269 |
+
self.g = Network()
|
270 |
+
self.g.add_nodes([0, 1, 2, 3])
|
271 |
+
|
272 |
+
def test_show_buttons(self):
|
273 |
+
self.assertFalse(self.g.options.configure["enabled"])
|
274 |
+
self.g.show_buttons(True)
|
275 |
+
self.assertTrue(self.g.options.configure["enabled"])
|
276 |
+
|
277 |
+
|
278 |
+
class EdgeOptionsTestCase(unittest.TestCase):
|
279 |
+
def setUp(self):
|
280 |
+
self.g = Network()
|
281 |
+
self.g.add_nodes([0, 1, 2, 3])
|
282 |
+
|
283 |
+
def test_set_edge_smooth(self):
|
284 |
+
self.assertEqual(self.g.options.edges.smooth.type, "dynamic")
|
285 |
+
self.g.set_edge_smooth("continuous")
|
286 |
+
self.assertEqual(self.g.options.edges.smooth.type, "continuous")
|
287 |
+
|
288 |
+
def test_inherit_colors(self):
|
289 |
+
self.assertTrue(self.g.options.edges.color.inherit)
|
290 |
+
self.g.inherit_edge_colors(False)
|
291 |
+
self.assertFalse(self.g.options.edges.color.inherit)
|
292 |
+
|
293 |
+
|
294 |
+
class LayoutTestCase(unittest.TestCase):
|
295 |
+
def setUp(self):
|
296 |
+
self.g = Network(layout=True)
|
297 |
+
|
298 |
+
def test_can_enable_init(self):
|
299 |
+
self.assertTrue(self.g.options["layout"])
|
300 |
+
|
301 |
+
def test_layout_disabled(self):
|
302 |
+
self.g = Network()
|
303 |
+
self.assertRaises(KeyError, lambda: self.g.options["layout"])
|
304 |
+
|
305 |
+
def test_levelSeparation(self):
|
306 |
+
self.assertTrue(self.g.options.layout.hierarchical.levelSeparation)
|
307 |
+
|
308 |
+
def test_treeSpacing(self):
|
309 |
+
self.assertTrue(self.g.options.layout.hierarchical.treeSpacing)
|
310 |
+
|
311 |
+
def test_blockShifting(self):
|
312 |
+
self.assertTrue(self.g.options.layout.hierarchical.blockShifting)
|
313 |
+
|
314 |
+
def test_edgeMinimization(self):
|
315 |
+
self.assertTrue(self.g.options.layout.hierarchical.edgeMinimization)
|
316 |
+
|
317 |
+
def test_parentCentralization(self):
|
318 |
+
self.assertTrue(self.g.options.layout.hierarchical.parentCentralization)
|
319 |
+
|
320 |
+
def test_sortMethod(self):
|
321 |
+
self.assertTrue(self.g.options.layout.hierarchical.sortMethod)
|
322 |
+
|
323 |
+
def test_set_edge_minimization(self):
|
324 |
+
self.g.options.layout.set_separation(10)
|
325 |
+
self.assertTrue(self.g.options.layout.hierarchical.levelSeparation == 10)
|
326 |
+
|
327 |
+
def test_set_tree_spacing(self):
|
328 |
+
self.g.options.layout.set_tree_spacing(10)
|
329 |
+
self.assertTrue(self.g.options.layout.hierarchical.treeSpacing == 10)
|
330 |
+
|
331 |
+
def test_set_edge_minimization(self):
|
332 |
+
self.g.options.layout.set_edge_minimization(True)
|
333 |
+
self.assertTrue(self.g.options.layout.hierarchical.edgeMinimization == True)
|
334 |
+
self.g.options.layout.set_edge_minimization(False)
|
335 |
+
self.assertTrue(self.g.options.layout.hierarchical.edgeMinimization == False)
|
pyvis/pyvis/tests/test_me.py
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
|
3 |
+
from ..network import Network
|
4 |
+
|
5 |
+
|
6 |
+
def test_canvas_size():
|
7 |
+
"""
|
8 |
+
Test the canvas size
|
9 |
+
"""
|
10 |
+
net = Network(500, 500)
|
11 |
+
assert net.width == 500 and net.height == 500
|
12 |
+
|
13 |
+
|
14 |
+
def test_add_node():
|
15 |
+
"""
|
16 |
+
Test adding a node to the network.
|
17 |
+
"""
|
18 |
+
net = Network()
|
19 |
+
|
20 |
+
net.add_node(0, "Test")
|
21 |
+
|
22 |
+
assert "Test" in net.nodes[0].values()
|
23 |
+
|
24 |
+
|
25 |
+
def test_add_ten_nodes():
|
26 |
+
"""
|
27 |
+
Test adding multiple nodes to this network
|
28 |
+
"""
|
29 |
+
net = Network()
|
30 |
+
|
31 |
+
for i in range(10):
|
32 |
+
net.add_node(i, "Test " + str(i))
|
33 |
+
|
34 |
+
assert len(net.nodes) == 10
|
35 |
+
|
36 |
+
|
37 |
+
def test_add_nodes_with_options():
|
38 |
+
"""
|
39 |
+
Test adding nodes with different options
|
40 |
+
"""
|
41 |
+
net = Network()
|
42 |
+
|
43 |
+
sizes = [10, 20, 30]
|
44 |
+
|
45 |
+
net.add_node(0, "Node 0", color="green", size=10)
|
46 |
+
net.add_node(1, "Node 1", color="blue", size=20)
|
47 |
+
net.add_node(2, "Node 2", color="yellow", size=30)
|
48 |
+
|
49 |
+
assert (sizes[node["id"]] == node["size"] for node in net.nodes)
|
50 |
+
|
51 |
+
|
52 |
+
def test_add_edge():
|
53 |
+
"""
|
54 |
+
Test adding an edge between nodes
|
55 |
+
"""
|
56 |
+
|
57 |
+
net = Network()
|
58 |
+
|
59 |
+
for i in range(10):
|
60 |
+
net.add_node(i, "Node " + str(i))
|
61 |
+
|
62 |
+
net.add_edge(0, 1)
|
63 |
+
net.add_edge(0, 2)
|
64 |
+
net.add_edge(0, 3)
|
65 |
+
net.add_edge(0, 4)
|
66 |
+
net.add_edge(0, 5)
|
67 |
+
net.add_edge(0, 6)
|
68 |
+
net.add_edge(0, 7)
|
69 |
+
net.add_edge(0, 8)
|
70 |
+
net.add_edge(0, 9)
|
71 |
+
|
72 |
+
assert net.get_adj_list()[0] == set([2, 1, 3, 4, 5, 6, 7, 8, 9])
|
73 |
+
|
74 |
+
|
75 |
+
def test_add_numpy_nodes():
|
76 |
+
"""
|
77 |
+
Test adding numpy array nodes since these
|
78 |
+
nodes will have specific numpy types
|
79 |
+
"""
|
80 |
+
arrayNodes = np.array([1, 2, 3, 4])
|
81 |
+
g = Network()
|
82 |
+
g.add_nodes(np.array([1, 2, 3, 4]))
|
83 |
+
assert g.get_nodes() == [1, 2, 3, 4]
|
pyvis/pyvis/utils.py
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# utility and helper functions for use in pyvis
|
2 |
+
|
3 |
+
|
4 |
+
def check_html(name):
|
5 |
+
"""
|
6 |
+
Given a name of graph to save or write, check if it is of valid syntax
|
7 |
+
|
8 |
+
:param: name: the name to check
|
9 |
+
:type name: str
|
10 |
+
"""
|
11 |
+
assert len(name.split(".")) >= 2, "invalid file type for %s" % name
|
12 |
+
assert name.split(".")[-1] == "html", "%s is not a valid html file" % name
|
pyvis/setup.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from setuptools import find_packages, setup
|
2 |
+
|
3 |
+
with open("README.md", "r", encoding="utf-8") as fh:
|
4 |
+
long_description = fh.read()
|
5 |
+
|
6 |
+
exec(open("pyvis/_version.py").read())
|
7 |
+
setup(
|
8 |
+
name="pyvis",
|
9 |
+
version=__version__,
|
10 |
+
description="A Python network graph visualization library",
|
11 |
+
long_description=long_description,
|
12 |
+
long_description_content_type="text/markdown",
|
13 |
+
url="https://github.com/WestHealth/pyvis",
|
14 |
+
author="Jose Unpingco",
|
15 |
+
author_email="datascience@westhealth.org",
|
16 |
+
license="BSD",
|
17 |
+
packages=find_packages(),
|
18 |
+
include_package_data=True,
|
19 |
+
install_requires=[
|
20 |
+
"jinja2 >= 2.9.6",
|
21 |
+
"networkx >= 1.11",
|
22 |
+
"ipython >= 5.3.0",
|
23 |
+
"jsonpickle >= 1.4.1",
|
24 |
+
],
|
25 |
+
python_requires=">3.6",
|
26 |
+
)
|