diff --git a/notebooks/custom_layout.json b/notebooks/custom_layout.json
new file mode 100644
index 0000000..e2922bb
--- /dev/null
+++ b/notebooks/custom_layout.json
@@ -0,0 +1,3 @@
+{
+ "color": "red"
+}
diff --git a/notebooks/layouts.ipynb b/notebooks/layouts.ipynb
new file mode 100644
index 0000000..05c575d
--- /dev/null
+++ b/notebooks/layouts.ipynb
@@ -0,0 +1,71 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "e0d2c1f7-3a10-4f30-a4d1-2d446de06487",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import stormvogel.layout\n",
+ "from stormvogel.layout import DEFAULT"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "8c98f637-ed9c-430b-b414-09c665b307f8",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'color': 'red'}\n",
+ "{'color': 'red'}\n",
+ "{'color': 'blue'}\n",
+ "{'color': 'blue'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Import custom layout\n",
+ "l1 = stormvogel.layout.Layout(custom=True, custom_path=\"custom_layout.json\")\n",
+ "l2 = stormvogel.layout.Layout(custom=True, custom_path=\"/home/ivo/git/stormvogel/notebooks/custom_layout.json\", custom_path_relative=False)\n",
+ "# Import template layout\n",
+ "l3 = stormvogel.layout.Layout(custom=False, template_path=\"layouts/default.json\")\n",
+ "l4 = stormvogel.layout.Layout(custom=False, template_path=DEFAULT)\n",
+ "l5 = "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9198a4b4-12cd-47a4-8950-278c18023192",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/model.html b/notebooks/model.html
index c163f89..03a4d07 100644
--- a/notebooks/model.html
+++ b/notebooks/model.html
@@ -294,7 +294,7 @@
// adding nodes and edges to the graph
data = {nodes: nodes, edges: edges};
- var options = {"nodes": {"color": {"background": "white", "border": "black"}}, "physics": {"barnesHut": {"gravitationalConstant": -22660, "centralGravity": 4.5, "springLength": 50, "springConstant": 0.08, "damping": 0.32, "avoidOverlap": 1}, "minVelocity": 0.75}};
+ var options = {"nodes": {"color": {"background": "blue", "border": "black"}}};
diff --git a/notebooks/study.ipynb b/notebooks/study.ipynb
index be5ee91..03737f4 100644
--- a/notebooks/study.ipynb
+++ b/notebooks/study.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
- "execution_count": 13,
+ "execution_count": 1,
"id": "0df1511e-565d-45d0-93a8-adafbfaaaefa",
"metadata": {},
"outputs": [
@@ -10,6 +10,15 @@
"name": "stdout",
"output_type": "stream",
"text": [
+ "\n",
+ "var options = {\n",
+ " \"nodes\": {\n",
+ " \"color\": {\n",
+ " \"background\": \"blue\",\n",
+ " \"border\": \"black\"\n",
+ " }\n",
+ " }\n",
+ "}\n",
"model.html\n"
]
},
@@ -28,7 +37,7 @@
" "
],
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
diff --git a/stormvogel/layout.py b/stormvogel/layout.py
new file mode 100644
index 0000000..0eb3eba
--- /dev/null
+++ b/stormvogel/layout.py
@@ -0,0 +1,71 @@
+"""Contains the code responsible for saving/loading layouts and modifying them interactively."""
+
+from pyvis.network import Network
+import os
+import json
+
+DEFAULT = "layouts/default.json"
+
+
+class Layout:
+ layout: dict
+
+ def __init__(
+ self,
+ custom: bool,
+ path: str | None = None,
+ path_relative: bool = True,
+ template_path: str = DEFAULT,
+ ) -> None:
+ """Load a new Layout from a json file. Use either a custom or a template file.
+
+ Args:
+ custom (bool, optional): If set to true, stormvogel will look for your custom layout.json file. Otherwise a template will be used.
+ path (str, optional): Relavant if custom is true. Path to your custom layout file, relative to the current working directory. Defaults to None.
+ path_relative (bool): Relavant if custom is true. If set to true, then stormvogel will look for a custom layout file relative to the current working directory.
+ template_path (str, optional): Relavant if custom is false. Path to a template layout files.
+ These are stored in the folder layouts. For simplicity, we recommed using the constants DEFAULT, etc.
+ Defaults to DEFAULT (="layouts/default.json").
+ """
+ if custom:
+ if path is None:
+ raise Exception(
+ "If custom is set to true, then the path needs to be set."
+ )
+ cwd = os.getcwd()
+ if path_relative:
+ complete_path = os.path.join(cwd, path)
+ else:
+ complete_path = path
+ with open(complete_path) as f:
+ json_string = f.read()
+ self.layout = json.loads(json_string)
+ else:
+ package_root_dir = os.path.dirname(os.path.realpath(__file__))
+ with open(os.path.join(package_root_dir, template_path)) as f:
+ json_string = f.read()
+ self.layout = json.loads(json_string)
+
+ def set_nt_layout(self, nt: Network) -> None:
+ """Set the layout of the network passed as the arugment."""
+ # We here use <> instead of {} because the f-string formatting already uses them.
+ option_string = f"""
+var options = <
+ "nodes": <
+ "color": <
+ "background": "{self.layout["color"]}",
+ "border": "black"
+ >
+ >
+>""".replace("<", "{").replace(">", "}")
+ print(option_string)
+ nt.set_options(option_string)
+
+ def save(self) -> None:
+ raise NotImplementedError()
+
+ def show_buttons(self) -> None:
+ raise NotImplementedError()
+
+ def __str__(self) -> str:
+ raise NotImplementedError()
diff --git a/stormvogel/layouts/default.json b/stormvogel/layouts/default.json
new file mode 100644
index 0000000..64d7a25
--- /dev/null
+++ b/stormvogel/layouts/default.json
@@ -0,0 +1,3 @@
+{
+ "color": "blue"
+}
diff --git a/stormvogel/visualization.py b/stormvogel/visualization.py
index 5d52510..f013c58 100644
--- a/stormvogel/visualization.py
+++ b/stormvogel/visualization.py
@@ -2,6 +2,7 @@
from pyvis.network import Network
from stormvogel.model import Model, EmptyAction, Number
+from stormvogel.layout import Layout
from ipywidgets import interact
from IPython.display import display
from fractions import Fraction
@@ -10,7 +11,10 @@
class Visualization:
"""Handles visualization of a Model using a pyvis Network."""
+ name: str
+ g: Network
ACTION_ID_OFFSET = 10**8
+ layout: Layout
# In the visualization, both actions and states are nodes with an id.
# This offset is used to keep their ids from colliding. It should be some high constant.
@@ -20,6 +24,7 @@ def __init__(
name: str = "model",
notebook: bool = True,
cdn_resources: str = "remote",
+ layout: Layout | None = None,
) -> None:
"""Create visualization of a Model using a pyvis Network
@@ -37,29 +42,12 @@ def __init__(
self.g = Network(notebook=notebook, directed=True, cdn_resources=cdn_resources)
self.__add_states()
self.__add_transitions()
- self.__set_layout()
-
- def __set_layout(self):
- self.g.set_options("""
-var options = {
- "nodes": {
- "color": {
- "background": "white",
- "border": "black"
- }
- },
- "physics": {
- "barnesHut": {
- "gravitationalConstant": -22660,
- "centralGravity": 4.5,
- "springLength": 50,
- "springConstant": 0.08,
- "damping": 0.32,
- "avoidOverlap": 1
- },
- "minVelocity": 0.75
- }
-}""")
+ if layout is None:
+ self.layout = Layout(custom=False)
+ else:
+ self.layout = layout
+
+ self.layout.set_nt_layout(self.g)
def __add_states(self):
"""For each state in the model, add a node to the graph."""