From dce39ef278e6b1aa32ecfb3e8776b94dabb12209 Mon Sep 17 00:00:00 2001 From: Bishalsarang Date: Wed, 14 Apr 2021 17:01:55 +0545 Subject: [PATCH 1/5] code-refactor: Refactor the code --- .gitignore | 4 ---- packaging.txt | 5 ----- 2 files changed, 9 deletions(-) delete mode 100644 packaging.txt diff --git a/.gitignore b/.gitignore index 9e41ecf..f2eae13 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ img out* *frames* -packaging.txt # Byte-compiled / optimized / DLL files __pycache__/ @@ -632,6 +631,3 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ - -!/packaging.txt -!/packaging.txt diff --git a/packaging.txt b/packaging.txt deleted file mode 100644 index ffbac37..0000000 --- a/packaging.txt +++ /dev/null @@ -1,5 +0,0 @@ -python3 setup.py sdist bdist_wheel - -twine upload --repository-url https://test.pypi.org/legacy/ dist/* - -twine upload dist/* From fc3baecef65b0fcd35ba53694affb88f1e7d179e Mon Sep 17 00:00:00 2001 From: Bishalsarang Date: Wed, 14 Apr 2021 19:33:16 +0545 Subject: [PATCH 2/5] feat-custom-node: Add custom node class for node operations --- .gitignore | 1 - tests/test_node.py | 40 +++++++++++++++++++++++++++++++++++++ visualiser/__init__.py | 6 ++++-- visualiser/node.py | 45 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 tests/test_node.py create mode 100644 visualiser/node.py diff --git a/.gitignore b/.gitignore index f2eae13..ee548d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -test* .idea .idea img diff --git a/tests/test_node.py b/tests/test_node.py new file mode 100644 index 0000000..1ea4926 --- /dev/null +++ b/tests/test_node.py @@ -0,0 +1,40 @@ +from visualiser import * + +""" + Test case class for nodes +""" +class TestNode: + def test_create_new_node(self): + node = Node('bishal', 'Bishal label', color='red', style='filled') + + assert node.label == 'Bishal label' + assert node.name == 'bishal' + assert node.get_attribute('color') == 'red' + assert node.get_attribute('style') == 'filled' + assert node.to_string() == 'bishal [label="Bishal label", color="red", style="filled"];' + + def test_set_attributes(self): + node = Node('bishal', 'Bishal label', color='red', style='filled') + assert node.get_attribute('color') == 'red' + node.set_attribute('color', 'green') + assert node.get_attribute('color') == 'green' + node.set_attribute('background', 'grey') + assert node.get_attribute('background') == 'grey' + assert node.to_string() == 'bishal [label="Bishal label", color="green", style="filled", background="grey"];' + + def test_rename_name_label(self): + node = Node('bishal', 'Bishal label', color='red', style='filled') + + node.set_attribute('color', 'green') + + assert node.label == 'Bishal label' + node.label = 'Bishal renamed label' + assert node.label == 'Bishal renamed label' + + assert node.name == 'bishal' + node.name = 'Bishal renamed' + assert node.name == 'Bishal renamed' + + assert node.to_string() == 'Bishal renamed [label="Bishal renamed label", color="green", style="filled"];' + + diff --git a/visualiser/__init__.py b/visualiser/__init__.py index 41ebf4c..cbadb0e 100644 --- a/visualiser/__init__.py +++ b/visualiser/__init__.py @@ -1,3 +1,5 @@ - # Version of the recursion-visualiser -__version__ = "1.0.1" \ No newline at end of file +__version__ = "1.0.1" + +from .node import Node +from visualiser import * diff --git a/visualiser/node.py b/visualiser/node.py new file mode 100644 index 0000000..fb6ae72 --- /dev/null +++ b/visualiser/node.py @@ -0,0 +1,45 @@ +class Node: + def __init__(self, name, label='', **attrs): + # TODO: Add support for initialization of attributes from attribute dict + self._name = name + self._attrs = attrs + + if len(label) == 0: + self._label = name + else: + self._label = label + + def __repr__(self): + return f"Node('{self.name}')" + + @property + def name(self): + return self._name + + @name.setter + def name(self, _name): + self._name = _name + + @property + def label(self): + return self._label + + @label.setter + def label(self, _label): + self._label = _label + + def get_attribute(self, key): + return self._attrs[key] + + def set_attribute(self, key, value): + self._attrs[key] = value + + def remove_attribute(self, key): + del self._attrs[key] + + def get_attributes_string(self): + return '[' + f'label="{self._label}", ' + ', '.join( + [f'{key}="{value}"' for key, value in self._attrs.items()]) + '];' + + def to_string(self): + return f'{self.name} {self.get_attributes_string()}' \ No newline at end of file From a5801790307436c1ebfb67cc5ba871d62ed11329 Mon Sep 17 00:00:00 2001 From: Bishalsarang Date: Wed, 14 Apr 2021 20:32:56 +0545 Subject: [PATCH 3/5] feat-custom-edge: Add custom edge class for edge operations --- tests/test_edge.py | 19 ++++++++ visualiser/__init__.py | 5 +- visualiser/edge.py | 103 +++++++++++++++++++++++++++++++++++++++++ visualiser/node.py | 10 ++-- 4 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 tests/test_edge.py create mode 100644 visualiser/edge.py diff --git a/tests/test_edge.py b/tests/test_edge.py new file mode 100644 index 0000000..28e7648 --- /dev/null +++ b/tests/test_edge.py @@ -0,0 +1,19 @@ +from visualiser import Edge, Node + +""" + Test case class for nodes +""" + + +class TestEdge: + def test_create_new_edge(self): + # Create Node + A = Node('A', color='red', style='filled') + B = Node('B', label='B label', color='red', style='filled') + + # Create Edge + edge = Edge(A, B, label='Test Label') + + assert edge.label == 'Test Label' + assert edge.source_node.label == 'A' + assert edge.source_node == A diff --git a/visualiser/__init__.py b/visualiser/__init__.py index cbadb0e..30e5717 100644 --- a/visualiser/__init__.py +++ b/visualiser/__init__.py @@ -1,5 +1,8 @@ # Version of the recursion-visualiser __version__ = "1.0.1" -from .node import Node +# Maintain this order to avoid circular import +# because we are using Node inside Edge for type annotations. +from .node import Node +from .edge import Edge from visualiser import * diff --git a/visualiser/edge.py b/visualiser/edge.py new file mode 100644 index 0000000..637f083 --- /dev/null +++ b/visualiser/edge.py @@ -0,0 +1,103 @@ +import copy + + +from visualiser import Node +class Edge: + + def __init__(self, source_node: Node, destination_node: Node, label: str = '', + **attrs: str) -> None: + # TODO: Remove copy after finding a better way to do this. + self._source_node = copy.deepcopy(source_node) + self._destination_node = copy.deepcopy(destination_node) + + self._label = label + self._attrs = attrs + + @property + def label(self) -> str: + """ + Get label for edge. + :return: str + """ + return self._label + + @label.setter + def label(self, _label: str) -> None: + """ + Set label for edge. + :param _label: str + """ + self._label = _label + + @property + def source_node(self) -> Node: + """ + Get source node + :return: Node + """ + return self._source_node + + @source_node.setter + def source_node(self, _source_node: Node) -> None: + """ + Set source node. + :param _source_node: Node + """ + self._source_node = _source_node + + @property + def destination_node(self) -> Node: + """ + Get destination node. + :return: Node + """ + return self._destination_node + + @destination_node.setter + def destination_node(self, _destination_node: Node) -> None: + """ + Sets destination node. + :param _destination_node: Node + """ + self._destination_node = _destination_node + + def get_attribute(self, key: str) -> str: + """ + Get attribute for edge. + :param key: str + :return: The value of attribute with key + """ + return self._attrs.get(key) + + def set_attribute(self, key: str, value: str) -> None: + """ + Set attribute for edge + :param key: str + :param value: str + """ + self._attrs[key] = value + + def remove_attribute(self, key: str) -> None: + """ + Remove attribute from edge. + :param key: str + """ + del self._attrs[key] + + def get_attributes_string(self) -> str: + """ + Get attributes string enclosed in [] + :return: + """ + if len(self._label) == 0: + return '[' + ', '.join([f'{key}="{value}"' for key, value in self._attrs.items()]) + ']' + + return '[' + f'label="{self._label}", ' + ', '.join( + [f'{key}="{value}"' for key, value in self._attrs.items()]) + ']' + + def to_string(self) -> str: + """ + Converts dot string equivalent of the current edge. + :return: Str + """ + return f'{self._source_node.name} -> {self._destination_node.name} {self.get_attributes_string()}' diff --git a/visualiser/node.py b/visualiser/node.py index fb6ae72..925db1e 100644 --- a/visualiser/node.py +++ b/visualiser/node.py @@ -1,7 +1,8 @@ + class Node: def __init__(self, name, label='', **attrs): # TODO: Add support for initialization of attributes from attribute dict - self._name = name + self._name = name self._attrs = attrs if len(label) == 0: @@ -12,6 +13,9 @@ def __init__(self, name, label='', **attrs): def __repr__(self): return f"Node('{self.name}')" + def __eq__(self, other): + return self.to_string() == other.to_string() + @property def name(self): return self._name @@ -29,7 +33,7 @@ def label(self, _label): self._label = _label def get_attribute(self, key): - return self._attrs[key] + return self._attrs.get(key) def set_attribute(self, key, value): self._attrs[key] = value @@ -42,4 +46,4 @@ def get_attributes_string(self): [f'{key}="{value}"' for key, value in self._attrs.items()]) + '];' def to_string(self): - return f'{self.name} {self.get_attributes_string()}' \ No newline at end of file + return f'{self.name} {self.get_attributes_string()}' From fee7fb9f2934fd205a3e2c317c81820c0c8dda02 Mon Sep 17 00:00:00 2001 From: Bishalsarang Date: Wed, 14 Apr 2021 22:40:53 +0545 Subject: [PATCH 4/5] feat-custom-graph: Add custom graph class for graph operations --- tests/test_graph.py | 35 ++++++++++++++ tests/test_node.py | 4 +- visualiser/__init__.py | 1 + visualiser/edge.py | 36 +++++++++++---- visualiser/graph.py | 102 +++++++++++++++++++++++++++++++++++++++++ visualiser/node.py | 1 + 6 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 tests/test_graph.py create mode 100644 visualiser/graph.py diff --git a/tests/test_graph.py b/tests/test_graph.py new file mode 100644 index 0000000..3b746cd --- /dev/null +++ b/tests/test_graph.py @@ -0,0 +1,35 @@ +from visualiser import Node, Edge, Graph + + +class TestGraph: + def test_normal_graph_flow(self): + graph = Graph('hello') + + # Create Node + A = Node('A', color='red') + B = Node('B', color='green') + C = Node('C', color='yellow') + + # Add node + graph.add_node(A) + graph.add_node(B) + graph.add_node(C) + + # Make edge + edge1 = Edge(A, B) + edge2 = Edge(A, C) + + # Add node and graph to edge + graph.add_edge(edge1) + graph.add_edge(edge2) + + assert graph.to_string() == 'digraph G {\nA [label="A", color="red"];\nB [label="B", color="green"];\nC [label="C", color="yellow"];\nA -> B [];\nA -> C [];\n}' + + def test_node_methods(self): + pass + + def test_edge_methods(self): + pass + + def test_mutations(self): + pass diff --git a/tests/test_node.py b/tests/test_node.py index 1ea4926..669ffcd 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -3,6 +3,8 @@ """ Test case class for nodes """ + + class TestNode: def test_create_new_node(self): node = Node('bishal', 'Bishal label', color='red', style='filled') @@ -36,5 +38,3 @@ def test_rename_name_label(self): assert node.name == 'Bishal renamed' assert node.to_string() == 'Bishal renamed [label="Bishal renamed label", color="green", style="filled"];' - - diff --git a/visualiser/__init__.py b/visualiser/__init__.py index 30e5717..652a7f0 100644 --- a/visualiser/__init__.py +++ b/visualiser/__init__.py @@ -5,4 +5,5 @@ # because we are using Node inside Edge for type annotations. from .node import Node from .edge import Edge +from .graph import Graph from visualiser import * diff --git a/visualiser/edge.py b/visualiser/edge.py index 637f083..7057a41 100644 --- a/visualiser/edge.py +++ b/visualiser/edge.py @@ -1,15 +1,18 @@ import copy +from typing import Union + +from visualiser import Node -from visualiser import Node class Edge: - def __init__(self, source_node: Node, destination_node: Node, label: str = '', + def __init__(self, source_node: Union[Node, str], destination_node: Union[Node, str], label: str = '', **attrs: str) -> None: - # TODO: Remove copy after finding a better way to do this. - self._source_node = copy.deepcopy(source_node) - self._destination_node = copy.deepcopy(destination_node) + self._source_node = Node(source_node) if isinstance(source_node, str) else copy.deepcopy(source_node) + self._destination_node = Node(destination_node) if isinstance(destination_node, str) else copy.deepcopy( + destination_node) + self._name = f"{self._source_node.name} -> {self._destination_node.name}" self._label = label self._attrs = attrs @@ -29,6 +32,22 @@ def label(self, _label: str) -> None: """ self._label = _label + @property + def name(self) -> str: + """ + Get name for edge. + :return: str + """ + return self._name + + @name.setter + def name(self, _name: str) -> None: + """ + Set label for edge. + :param _name: str + """ + self._name = _name + @property def source_node(self) -> Node: """ @@ -82,7 +101,8 @@ def remove_attribute(self, key: str) -> None: Remove attribute from edge. :param key: str """ - del self._attrs[key] + if self._attrs.get(key): + del self._attrs[key] def get_attributes_string(self) -> str: """ @@ -90,10 +110,10 @@ def get_attributes_string(self) -> str: :return: """ if len(self._label) == 0: - return '[' + ', '.join([f'{key}="{value}"' for key, value in self._attrs.items()]) + ']' + return '[' + ', '.join([f'{key}="{value}"' for key, value in self._attrs.items()]) + '];' return '[' + f'label="{self._label}", ' + ', '.join( - [f'{key}="{value}"' for key, value in self._attrs.items()]) + ']' + [f'{key}="{value}"' for key, value in self._attrs.items()]) + '];' def to_string(self) -> str: """ diff --git a/visualiser/graph.py b/visualiser/graph.py new file mode 100644 index 0000000..a9338b0 --- /dev/null +++ b/visualiser/graph.py @@ -0,0 +1,102 @@ +import copy + +from visualiser import Node, Edge + + +class Graph: + def __init__(self, name='', **attrs): + self._name = name + self._attrs = attrs + + self._nodes = [] + self._edges = [] + + def get_node(self, name): + filtered_nodes = list(filter(lambda node: node.name == name, self._nodes)) + + return filtered_nodes[0] if len(filtered_nodes) else None + + def remove_node(self, _node): + if isinstance(_node, Node): + self._nodes = list(filter(lambda node: node.name != _node.name, self._nodes)) + return + + self._nodes = list(filter(lambda node: node.name != _node, self._nodes)) + + def add_node(self, node): + if self.get_node(node.name) is not None: + self.remove_node(node) + + self._nodes.append(copy.deepcopy(node)) + + def set_node_attributes(self, node, **attrs): + for key, value in attrs.items(): + self.set_node_attribute(node, key, value) + + def set_node_attribute(self, _node, key, value): + def _set_attribute(name): + node = self.get_node(name) + if node: + node.set_attribute(key, value) + + if isinstance(_node, Node): + _set_attribute(_node.name) + return + + _set_attribute(_node) + + def remove_node_attribute(self, _node, key): + def _remove_attribute(name): + node = self.get_node(name) + if node: + node.remove_attribute(key) + + if isinstance(_node, Node): + _remove_attribute(_node.name) + return + + _remove_attribute(_node) + + def add_edge(self, edge): + if self.get_edge(edge.name) is not None: + self.remove_edge(edge) + + # TODO: Check if node used in node exist in graph. If not create one + self._edges.append(copy.deepcopy(edge)) + + def get_edge(self, name): + filtered_edges = list(filter(lambda edge: edge.name == name, self._edges)) + + return filtered_edges[0] if len(filtered_edges) else None + + def remove_edge(self, _edge): + if isinstance(_edge, Edge): + self._edges = list(filter(lambda edge: edge.name != _edge.name, self._edges)) + return + + self._edges = list(filter(lambda edge: edge.name != _edge, self._edges)) + + def set_edge_label(self, name, value): + edge = self.get_edge(name) + if edge: + edge.label = value + + def highlight_node(self, name, color): + self.set_node_attribute(name, 'color', color) + + def highlight_edge(self, name, color): + edge = self.get_edge(name) + if edge: + edge.set_attribute('color', color) + + def reverse_edge_orientation(self, name): + pass + + def get_nodes_string(self): + return "\n".join(list(map(lambda node: node.to_string(), self._nodes))) + + def get_edges_string(self): + return "\n".join(list(map(lambda edge: edge.to_string(), self._edges))) + + def to_string(self): + return "digraph G {\n" + f"{self.get_nodes_string()}\n" + f"{self.get_edges_string()}\n" + "}" diff --git a/visualiser/node.py b/visualiser/node.py index 925db1e..d5469cd 100644 --- a/visualiser/node.py +++ b/visualiser/node.py @@ -42,6 +42,7 @@ def remove_attribute(self, key): del self._attrs[key] def get_attributes_string(self): + # TODO: Check if no any attributes and labels to avoid '[]' return '[' + f'label="{self._label}", ' + ', '.join( [f'{key}="{value}"' for key, value in self._attrs.items()]) + '];' From 6a6312c9476a5ab95adb2f13755938274dcfbe15 Mon Sep 17 00:00:00 2001 From: Bishalsarang Date: Sun, 18 Apr 2021 00:14:42 +0545 Subject: [PATCH 5/5] feat-custom-animation: Add custom animation class for animation operations --- visualiser/__init__.py | 2 ++ visualiser/animation.py | 68 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 visualiser/animation.py diff --git a/visualiser/__init__.py b/visualiser/__init__.py index 652a7f0..d93fddc 100644 --- a/visualiser/__init__.py +++ b/visualiser/__init__.py @@ -6,4 +6,6 @@ from .node import Node from .edge import Edge from .graph import Graph +from .animation import Animation + from visualiser import * diff --git a/visualiser/animation.py b/visualiser/animation.py new file mode 100644 index 0000000..7e60cb8 --- /dev/null +++ b/visualiser/animation.py @@ -0,0 +1,68 @@ +import copy + +import pydot +import glob +import os +import shutil +import imageio + +from visualiser import Node, Edge, Graph + + +class Animation: + def __init__(self): + self.frames = [] + self._frame_id = 0 + + def next_step(self, frame): + self.frames.append(copy.deepcopy(frame)) + self._frame_id += 1 + + @staticmethod + def make_directory(): + if not os.path.exists("frames"): + os.makedirs("frames") + + def next_frame(self): + pass + + def previous_frame(self): + pass + + def get_frame(self, frame_id): + return self.frames[frame_id] + + def get_frames(self): + return self.frames[::] + + def write_frame(self, frame_id): + frame = self.get_frame(frame_id) + + dot_graph, *rest = pydot.graph_from_dot_data(frame) + dot_graph.write_png(f"frames/temp_{frame_id}.png") + + def write_file(self): + self.make_directory() + + for frame_id in range(len(self.get_frames())): + self.write_frame(frame_id) + + def write_gif(self, name="out.gif", delay=3): + self.write_file() + + images = [] + + # sort frames images in ascending order to number in image filename + # image filename: frames/temp_1.png + sorted_images = sorted( + glob.glob("frames/*.png"), + key=lambda fn: int(fn.split("_")[1].split(".")[0]) + ) + + for filename in sorted_images: + images.append(imageio.imread(filename)) + + imageio.mimsave(name, images, duration=delay) + # Delete temporary directory + shutil.rmtree("frames") +