From 6950e783dea428185e0972e18f477a380c6c1366 Mon Sep 17 00:00:00 2001 From: YouGuessedMyName Date: Mon, 1 Jul 2024 16:24:59 +0200 Subject: [PATCH] Progress on saving layouts --- notebooks/custom_layout.json | 3 ++ notebooks/layouts.ipynb | 71 +++++++++++++++++++++++++++++++++ notebooks/model.html | 2 +- notebooks/study.ipynb | 13 +++++- stormvogel/layout.py | 71 +++++++++++++++++++++++++++++++++ stormvogel/layouts/default.json | 3 ++ stormvogel/visualization.py | 34 +++++----------- 7 files changed, 171 insertions(+), 26 deletions(-) create mode 100644 notebooks/custom_layout.json create mode 100644 notebooks/layouts.ipynb create mode 100644 stormvogel/layout.py create mode 100644 stormvogel/layouts/default.json 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."""