Repository: rudnam/atg-visualizer
Pull request #33: Test posetvisualizer
Author: login: hcantonio1
Created: 2025-02-28T13:42:28Z | Merged: 2025-03-03T02:23:12Z

- create RENDER_TYPE_SPECIFICATIONS
- small changes to FigureTester
- create tests for the functions PosetVisualizer(), .select_nodes, and .select_and_highlight_nodes

TODO:
- either move the assertions to the "underscore" functions or make them return a reason for test failure


Diff:
diff --git a/backend/app/posetvisualizer.py b/backend/app/posetvisualizer.py
index 845e534..53159a4 100644
--- a/backend/app/posetvisualizer.py
+++ b/backend/app/posetvisualizer.py
@@ -1,12 +1,66 @@
+from typing import TypedDict, Literal
 from itertools import permutations, combinations, chain
 import networkx as nx
 import plotly.graph_objects as go
 import seaborn as sns
 
+from app.classes import *
+
+
+class NodeRenderSpecification(TypedDict):
+    color: str
+    size: int
+    opacity: float
+
+
+class EdgeRenderSpecification(TypedDict):
+    color: str
+    width: int
+    opacity: float
+
+
+class EdgeRenderSpecificationForSelectedOther(TypedDict):
+    width: int
+    opacity: float
+
+
+class NodeRenderTypeSpecs(TypedDict):
+    selected: NodeRenderSpecification
+    highlighted: NodeRenderSpecification
+    other: NodeRenderSpecification
+
+
+class EdgeRenderTypeSpecs(TypedDict):
+    selected: EdgeRenderSpecificationForSelectedOther
+    highlighted: EdgeRenderSpecification
+    other: EdgeRenderSpecificationForSelectedOther
+
+
+class RenderTypeSpecs(TypedDict):
+    node: NodeRenderTypeSpecs
+    edge: EdgeRenderTypeSpecs
+
+
+type RenderType = Literal["selected", "highlighted", "other"]
+
 
 class PosetVisualizer:
     MAX_SIZE = 8
 
+    RENDER_TYPE_SPECS: RenderTypeSpecs = {
+        "node": {
+            "selected": {"color": "black", "size": 3, "opacity": 1},
+            "highlighted": {"color": "red", "size": 6, "opacity": 1},
+            "other": {"color": "black", "size": 3, "opacity": 0.1},
+        },
+        "edge": {
+            "selected": {"width": 3, "opacity": 1},
+            "highlighted": {"color": "red", "width": 5, "opacity": 1},
+            "other": {"width": 3, "opacity": 0.1},
+        },
+        # showlegend is also affected by render_type but not specified in this specs
+    }
+
     def __init__(self, size: int):
         if not isinstance(size, int):
             raise TypeError("Invalid size. Size must be an integer.")
@@ -15,20 +69,29 @@ def __init__(self, size: int):
                 f"Invalid size. Size must have a value of at least 2 and at most {self.MAX_SIZE}."
             )
 
-        self.size = size
-        self.selected_nodes = []
-        self.highlighted_nodes = []
+        self.size: int = size
+
+        self.selected_nodes: list[LinearOrder] = []
+        self.highlighted_nodes: list[LinearOrder] = []
+
+        self._graph: nx.Graph
+        # for the next properties, str means '1', '2', '3', etc. except for the values of _pair_to_color which are of course colors
+        self._pair_to_color: dict[tuple[str, str], str]
+        self._edges_by_swap: dict[
+            tuple[str, str], list[tuple[LinearOrder, LinearOrder]]
+        ]
+        self._fig: go.Figure
 
         self._set_up_graph()
         self._pos = nx.spring_layout(self._graph, dim=3, seed=42)
-        self._edge_trace = self._compute_edge_traces()
-        self._node_trace = self._compute_node_traces()
-        self._fig_layout = self._make_fig_layout()
+        self._edge_trace: list[go.Scatter3d] = self._compute_edge_traces()
+        self._node_trace: list[go.Scatter3d] = self._compute_node_traces()
+        self._fig_layout: go.Layout = self._make_fig_layout()
 
         self.update_figure()
 
     @staticmethod
-    def _get_swapped_numbers(p1: str, p2: str):
+    def _get_swapped_numbers(p1: str, p2: str) -> tuple[str, str]:
         """
         Returns the two numbers that were swapped between permutations,
         or None if not a valid adjacent transposition
@@ -57,7 +120,7 @@ def _generate_colors(n_colors: int):
 
     def _set_up_graph(self):
         """Sets up graph and swap types"""
-        sequence = "".join(map(str, range(1, self.size + 1)))
+        sequence: str = "".join(map(str, range(1, self.size + 1)))
         number_pairs = list(combinations(sequence, 2))
         perm_strings = ["".join(p) for p in permutations(sequence)]
         self._graph = nx.Graph()
@@ -128,7 +191,9 @@ def _compute_edge_traces(self):
 
         return edge_traces
 
-    def _make_edge_trace(self, edges, render_type="selected", number_pair=("1", "2")):
+    def _make_edge_trace(
+        self, edges, render_type: RenderType = "selected", number_pair=("1", "2")
+    ):
         selected_edges_x = []
         selected_edges_y = []
         selected_edges_z = []
@@ -143,12 +208,18 @@ def _make_edge_trace(self, edges, render_type="selected", number_pair=("1", "2")
             selected_edges_z.extend(z_coords)
 
         # set trace options according to render type
-        line_dict = (
-            dict(color="red", width=5, dash="solid")
+        edge_color_by_swap = self._pair_to_color[number_pair]
+        edge_color_highlighted = self.RENDER_TYPE_SPECS["edge"]["highlighted"]["color"]
+        edge_color = (
+            edge_color_highlighted
             if render_type == "highlighted"
-            else dict(color=self._pair_to_color[number_pair], width=2)
+            else edge_color_by_swap
         )
-        trace_opacity = 0.1 if render_type == "other" else 1
+        line_dict = dict(
+            color=edge_color,
+            width=self.RENDER_TYPE_SPECS["edge"][render_type]["width"],
+        )
+        trace_opacity = self.RENDER_TYPE_SPECS["edge"][render_type]["opacity"]
         trace_show_legend = render_type == "selected"
 
         return go.Scatter3d(
@@ -198,14 +269,12 @@ def node_is_other(node):
             node_traces.append(self._make_node_trace(other_nodes, render_type="other"))
         return node_traces
 
-    def _make_node_trace(self, nodes, render_type="selected"):
-        # set trace options according to render type
-        marker_dict = (
-            dict(size=6, color="red")
-            if render_type == "highlighted"
-            else dict(size=3, color="black")
+    def _make_node_trace(self, nodes, render_type: RenderType = "selected"):
+        marker_dict = dict(
+            color=self.RENDER_TYPE_SPECS["node"][render_type]["color"],
+            size=self.RENDER_TYPE_SPECS["node"][render_type]["size"],
         )
-        trace_opacity = 0.1 if render_type == "other" else 1
+        trace_opacity = self.RENDER_TYPE_SPECS["node"][render_type]["opacity"]
         trace_show_legend = render_type == "selected"
 
         selected_nodes_x = []
@@ -253,14 +322,14 @@ def _make_fig_layout(self):
     def _all_perms_are_selected(self):
         return len(self.selected_nodes) == 0
 
-    def select_nodes(self, select_nodes: list[str]):
+    def select_nodes(self, select_nodes: list[LinearOrder]):
         """Selects specified nodes"""
         self.selected_nodes = select_nodes
         self._node_trace = self._compute_node_traces()
         self._edge_trace = self._compute_edge_traces()
         self.update_figure()
 
-    def highlight_nodes(self, select_nodes: list[str]):
+    def highlight_nodes(self, select_nodes: list[LinearOrder]):
         """Highlights specified nodes"""
         self.highlighted_nodes = select_nodes
         self._node_trace = self._compute_node_traces()
@@ -268,7 +337,7 @@ def highlight_nodes(self, select_nodes: list[str]):
         self.update_figure()
 
     def select_and_highlight_nodes(
-        self, select_nodes: list[str], highlight_nodes: list[str]
+        self, select_nodes: list[LinearOrder], highlight_nodes: list[LinearOrder]
     ):
         self.highlighted_nodes = highlight_nodes
         self.selected_nodes = select_nodes
diff --git a/backend/tests/test_posetvisualizer.py b/backend/tests/test_posetvisualizer.py
index 64c0d23..ba768a3 100644
--- a/backend/tests/test_posetvisualizer.py
+++ b/backend/tests/test_posetvisualizer.py
@@ -1,10 +1,230 @@
+"""
+Test Overview
+
+Public interface of a PosetVisualizer (✓ - for testing; ✗ - maybe not)
+ - select_nodes                 ✓ function is useful
+ - highlight_nodes              ✗ not useful on its own
+ - select_and_highlight_nodes   ✓ function is useful
+ - update_figure                ✗ used only by the select node methods
+ - show_figure                  ✗ not used
+ - get_figure_data              ✓ main purpose of the visualizer
+
+Some notes regarding the purpose of the select-node functions
+    There are three classes of vertices and edges: selected, highlighted, and others
+    All vertices and edges start as "others"
+    select_nodes will move some vertices (and their edges) to "selected" class/status
+    highlight_nodes will move some vertices (and their edges) to "highlighted" class/status
+    select_and_highlight_nodes does both
+    Only then is get_figure called (see main.py)
+
+
+Charateristics of Nodes and Edges according to Class
+    +-------------------+-------------------+-------------------+
+    |       Class       |      Vertex       |       Edge        |
+    +-------------------+-------------------+-------------------+
+    |     Selected      |   color="black"   |color=<swap_color> |
+    |                   |      size=3       |      width=2      |
+    |                   |     opacity=1     |     opacity=1     |
+    +-------------------+-------------------+-------------------+
+    |    Highlighted    |    color="red"    |    color="red"    |
+    |                   |      size=6       |      width=5      |
+    |                   |     opacity=1     |     opacity=1     |
+    +-------------------+-------------------+-------------------+
+    |      Others       |   color="black"   |color=<swap_color> |
+    |                   |      size=3       |      width=2      |
+    |                   |    opacity=0.1    |    opacity=0.1    |
+    +-------------------+-------------------+-------------------+
+
+    Observations after investigating the traces.
+        Except for the variable color (<swap_color>), colors red and black can be verified as literals "red" and "black"
+        Then other attributes can be check for their numerical value
+
+New Test Proposal
+Now, since it is clear we are operating with three render types, I suppose we should not test for the definition anymore.
+(Notice that the ATG in the paper does not deal with highlighted edges, nor "other" nodes.)
+Instead, we should simply test for the render types and hope that our representation still contains the ATG.
+It should also be noted that there is no test for .get_figure_data as its reliability is guaranteed by all the tests.
+1. test_errors_raised
+2. test "default" output (i.e. without selection; permutahedron). effectively the same as .select_nodes(<all_nodes>)
+    Call PosetVisualizer(n) for n = 3 and 4
+    a. nodes (selected)
+    b. edges (selected, does not exist)
+3. test select_nodes
+    Call PosetVisualizer(n) for n = 3 and 4, then call .select_nodes()
+    a. nodes (selected, other)
+    b. edges (selected, other, does not exist)
+4. test select_and_highlight_nodes()
+    Call PosetVisualizer(n) for n = 3 and 4, then call .select_and_highlight_nodes()
+    a. nodes (selected, highlighted, other)
+    b. edges (selected, highlighted, other, does not exist)
+"""
+
 import pytest
 
-from app.posetvisualizer import PosetVisualizer
+from app.posetvisualizer import PosetVisualizer, RenderType
 from tests.testing_utils import FigureTester
 
+from .upsilon_constants import TWOMAXIMAL
+
+RENDER_TYPE_SPECS = PosetVisualizer.RENDER_TYPE_SPECS
+
+
+def verify_edges_have_the_same_color(
+    tester: FigureTester, edges: list[tuple[str, str]]
+) -> None:
+    """Verify all edges provided have the same color. Not applicable to lists containing highlighted edges.
+
+    Equivalent to verifying that all edges have the same swap, i.e. same equivalence class
+    """
+    edge0 = edges[0]
+    edge_data0 = tester.get_edge_data(*edge0)
+
+    assert (
+        edge_data0
+    ), f"edge '{edge0}', while expected to exist, does not exist in figure data"
+
+    color0 = edge_data0["line"]["color"]
+    for edge in edges[1:]:
+        edge_data = tester.get_edge_data(*edge)
+
+        assert (
+            edge_data
+        ), f"edge '{edge}', while expected to exist, does not exist in figure data"
+
+        color = edge_data["line"]["color"]
+
+        assert (
+            color == color0
+        ), f"expected {{edge: {edge}, color: {color} }} to have the same color as {{edge0: {edge0}, color: {color0} }}"
+
+
+def verify_edges_have_different_colors(
+    tester: FigureTester, edge1: tuple[str, str], edge2: tuple[str, str]
+) -> None:
+    """Verify that TWO edges differ in color. Not applicable to highlighted edges"""
+    edge_data1 = tester.get_edge_data(*edge1)
+    edge_data2 = tester.get_edge_data(*edge2)
+
+    assert (
+        edge_data1
+    ), f"edge '{edge1}', while expected to exist, does not exist in figure data"
+    assert (
+        edge_data2
+    ), f"edge '{edge2}', while expected to exist, does not exist in figure data"
+
+    color1 = edge_data1["line"]["color"]
+    color2 = edge_data2["line"]["color"]
+
+    assert (
+        color1 != color2
+    ), f"expected {{edge1: {edge1}, color: {color1} }} to have a different color than {{edge2: {edge2}, color: {color2} }}"
+
+
+def verify_edges_do_not_exist(tester, edges) -> None:
+    """Verify if all edges provided are NOT part of the atg
+
+    Any two permutations with one unreachable by an adjacent swap from the other should not constitute an edge\\
+    That is, an "edge" between such permutations should not be part of the atg
+
+    This function is usually used to check if the "edge" adheres to the definition of atg's\\
+    In the future, we should perhaps check for edges not found in the supercover,
+        or simply create new functions
+    """
+    for node_text1, node_text2 in edges:
+        edge_data = tester.get_edge_data(node_text1, node_text2)
+        assert (
+            edge_data is None
+        ), f"edge '{(node_text1, node_text2)}', while expected to not exist, does exist in figure data"
+
+
+def verify_render_type_of_node(
+    tester: FigureTester, node_text: str, render_type: RenderType
+) -> bool:
+    """Verify that the node is rendered as render_type
+
+    Args:
+        tester: The object containing the traces
+        node_text: The text of the node like '1234', a LinearOrder
+        render_type: The expected render type of the node
+
+    Returns:
+        bool:
+    """
+    node_data = tester.get_node_data(node_text)
+    assert (
+        node_data
+    ), f"node '{node_text}', while expected to exist, does not exist in figure data"
+
+    render_type_color = RENDER_TYPE_SPECS["node"][render_type]["color"]
+    render_type_size = RENDER_TYPE_SPECS["node"][render_type]["size"]
+    render_type_opacity = RENDER_TYPE_SPECS["node"][render_type]["opacity"]
+    color = node_data["marker"]["color"]
+    size = node_data["marker"]["size"]
+    opacity = node_data["opacity"]
+
+    same_color = color == render_type_color
+    same_size = size == render_type_size
+    same_opacity = opacity == render_type_opacity
+    return same_color and same_size and same_opacity
+
+
+def verify_render_type_of_edge(
+    tester: FigureTester, edge: tuple[str, str], render_type: RenderType
+) -> bool:
+    """Verify that the edge is rendered as render_type
+
+    Args:
+        tester: The object containing the traces
+        edge: An edge described by the text of its nodes, e.g. tuple('123','213')
+        render_type: The expected render type of the edge
+
+    Returns:
+        bool:
+    """
+    node_text1, node_text2 = edge
+    edge_data = tester.get_edge_data(node_text1, node_text2)
+    assert (
+        edge_data
+    ), f"edge '{(node_text1, node_text2)}', while expected to exist, does not exist in figure data"
+
+    highlighted_color = RENDER_TYPE_SPECS["edge"]["highlighted"]["color"]
+    render_type_width = RENDER_TYPE_SPECS["edge"][render_type]["width"]
+    render_type_opacity = RENDER_TYPE_SPECS["edge"][render_type]["opacity"]
+    color = edge_data["line"]["color"]
+    width = edge_data["line"]["width"]
+    opacity = edge_data["opacity"]
+
+    color_is_ok = render_type != "highlighted" or color == highlighted_color
+    same_width = width == render_type_width
+    same_opacity = opacity == render_type_opacity
+    return color_is_ok and same_width and same_opacity
 
-def testPosetVisualizer():
+
+def verify_render_type_of_nodes(
+    tester: FigureTester, nodes: list[str], render_type: RenderType
+) -> None:
+    """Verify that the nodes are rendered as render_type"""
+    for node_text in nodes:
+        is_correctly_rendered = verify_render_type_of_node(
+            tester, node_text, render_type
+        )
+        assert (
+            is_correctly_rendered
+        ), f'expected node "{node_text}" is rendered as "{render_type}", but it\'s not'
+
+
+def verify_render_type_of_edges(
+    tester: FigureTester, edges: list[tuple[str, str]], render_type: RenderType
+) -> None:
+    """Verify that the edges are rendered as render_type"""
+    for edge in edges:
+        is_correctly_rendered = verify_render_type_of_edge(tester, edge, render_type)
+        assert (
+            is_correctly_rendered
+        ), f'expected edge "{edge}" is rendered as "{render_type}", but it\'s not'
+
+
+def test_errors_raised():
     with pytest.raises(Exception, match="at least 2"):
         PosetVisualizer(1)
 
@@ -14,38 +234,204 @@ def testPosetVisualizer():
     with pytest.raises(TypeError, match="must be an integer"):
         PosetVisualizer("4")
 
+
+def test_default_behavior():
     # ============ Size 3 ============ #
     visualizer = PosetVisualizer(3)
     tester = FigureTester(visualizer.get_figure_data())
 
-    # Edges only exist between adjacent swaps
-    assert tester.get_edge_data("123", "132") != None
-    assert tester.get_edge_data("123", "321") == None
+    # selected/highlighted nodes are part of the atg
+    # for this case, there are no "highlighted", "other", and "does not exist" nodes to check
+    selected_nodes = ["123", "132", "213", "231", "312", "321"]
+    verify_render_type_of_nodes(tester, selected_nodes, render_type="selected")
 
-    # Edges are colored according to swap type
-    assert (
-        tester.get_edge_data("123", "132")["line"]["color"]
-        == tester.get_edge_data("231", "321")["line"]["color"]
-    )
-    assert (
-        tester.get_edge_data("123", "132")["line"]["color"]
-        != tester.get_edge_data("123", "213")["line"]["color"]
-    )
+    # selected/highlighted edges are part of the atg, other edges are "visual aids"
+    # for this case, there are no "highlighted" and "other" edges to check
+    selected_edges = [("123", "132"), ("213", "231"), ("312", "321")]
+    some_edges_that_dont_exist = [
+        ("123", "312"),
+        ("123", "321"),
+        ("123", "231"),
+        ("312", "213"),
+    ]
+    verify_render_type_of_edges(tester, selected_edges, render_type="selected")
+    verify_edges_do_not_exist(tester, some_edges_that_dont_exist)
+
+    # edges are colored according to swap type except highlighted edges
+    edge_12 = [("123", "213"), ("312", "321")]
+    edge_23 = [("123", "132"), ("231", "321")]
+    diff_swap = [("123", "132"), ("123", "213")]
+    verify_edges_have_the_same_color(tester, edge_23)
+    verify_edges_have_the_same_color(tester, edge_12)
+    verify_edges_have_different_colors(tester, *diff_swap)
 
     # ============ Size 4 ============ #
     visualizer = PosetVisualizer(4)
     tester = FigureTester(visualizer.get_figure_data())
 
-    # Edges only exist between adjacent swaps
-    assert tester.get_edge_data("1234", "1324") != None
-    assert tester.get_edge_data("1234", "3214") == None
+    # selected/highlighted nodes are part of the atg
+    # for this case, there are no "highlighted", "other", and "does not exist" nodes to check
+    some_selected_nodes = ["1243", "1432", "2134", "2431", "4312", "3214"]
+    verify_render_type_of_nodes(tester, some_selected_nodes, render_type="selected")
 
-    # Edges are colored according to swap type
-    assert (
-        tester.get_edge_data("1234", "1324")["line"]["color"]
-        == tester.get_edge_data("4231", "4321")["line"]["color"]
-    )
-    assert (
-        tester.get_edge_data("1234", "1324")["line"]["color"]
-        != tester.get_edge_data("1234", "2134")["line"]["color"]
-    )
+    # selected/highlighted edges are part of the atg, other edges are "visual aids"
+    # for this case, there are no "highlighted" and "other" edges to check
+    some_selected_edges = [("1423", "1432"), ("2134", "2143"), ("4312", "4321")]
+    some_edges_that_dont_exist = [
+        ("1234", "3124"),
+        ("1423", "3421"),
+        ("1243", "2341"),
+        ("4312", "4213"),
+    ]
+    verify_render_type_of_edges(tester, some_selected_edges, render_type="selected")
+    verify_edges_do_not_exist(tester, some_edges_that_dont_exist)
+
+    # edges are colored according to swap type except highlighted edges
+    edge_14 = [("2143", "2413"), ("3214", "3241"), ("1423", "4123")]
+    edge_24 = [("1243", "1423"), ("2413", "4213"), ("1324", "1342")]
+    diff_swap = [("1243", "1423"), ("1423", "4123")]
+    verify_edges_have_the_same_color(tester, edge_14)
+    verify_edges_have_the_same_color(tester, edge_24)
+    verify_edges_have_different_colors(tester, *diff_swap)
+
+
+def test_select_nodes():
+    # ============ Size 3 ============ #
+    visualizer = PosetVisualizer(3)
+    selected_nodes = ["123", "132", "312", "321"]
+    visualizer.select_nodes(selected_nodes)
+    tester = FigureTester(visualizer.get_figure_data())
+
+    # selected/highlighted nodes are part of the atg
+    # for this case, there are no "highlighted", and "does not exist" nodes to check
+    selected_nodes = selected_nodes
+    other_nodes = ["231", "213"]
+    verify_render_type_of_nodes(tester, selected_nodes, render_type="selected")
+    verify_render_type_of_nodes(tester, other_nodes, render_type="other")
+
+    # selected edges are part of the atg, other edges are "visual aids"
+    # for this case, there are no "highlighted" edges to check
+    selected_edges = [("123", "132"), ("132", "312"), ("312", "321")]
+    other_edges = [("321", "231"), ("231", "213"), ("213", "123")]
+    some_edges_that_dont_exist = [
+        ("123", "312"),
+        ("123", "321"),
+        ("123", "231"),
+        ("312", "213"),
+    ]
+    verify_render_type_of_edges(tester, selected_edges, render_type="selected")
+    verify_render_type_of_edges(tester, other_edges, render_type="other")
+    verify_edges_do_not_exist(tester, some_edges_that_dont_exist)
+
+    # edges are colored according to swap type except highlighted edges
+    # in this case, no "selected" edges have the same color
+    diff_swap1 = [("123", "132"), ("132", "312")]
+    diff_swap2 = [("123", "132"), ("312", "321")]
+    diff_swap3 = [("132", "312"), ("312", "321")]
+    verify_edges_have_different_colors(tester, *diff_swap1)
+    verify_edges_have_different_colors(tester, *diff_swap2)
+    verify_edges_have_different_colors(tester, *diff_swap3)
+
+    # ============ Size 4 ============ #
+    visualizer = PosetVisualizer(4)
+    selected_nodes = TWOMAXIMAL
+    visualizer.select_nodes(selected_nodes)
+    tester = FigureTester(visualizer.get_figure_data())
+
+    # selected/highlighted nodes are part of the atg
+    # for this case, there are no "highlighted" and "does not exist" nodes to check
+    selected_nodes = selected_nodes
+    some_other_nodes = ["4321", "4231", "2413"]
+    verify_render_type_of_nodes(tester, selected_nodes, render_type="selected")
+    verify_render_type_of_nodes(tester, some_other_nodes, render_type="other")
+
+    # selected/highlighted edges are part of the atg, other edges are "visual aids"
+    # for this case, there are no "highlighted" edges to check
+    some_selected_edges = [("2134", "2143"), ("1234", "1324"), ("3142", "3412")]
+    some_other_edges = [("4321", "4231"), ("2143", "2413")]
+    some_edges_that_dont_exist = [("1243", "3142"), ("4123", "4321"), ("1234", "2314")]
+    verify_render_type_of_edges(tester, some_selected_edges, render_type="selected")
+    verify_render_type_of_edges(tester, some_other_edges, render_type="other")
+    verify_edges_do_not_exist(tester, some_edges_that_dont_exist)
+
+    # edges are colored according to swap type except highlighted edges
+    edge_24 = [("3124", "3142"), ("1324", "1342"), ("1243", "1423")]
+    edge_13 = [("3124", "1324"), ("3142", "1342")]
+    edge_23 = [("1423", "1432"), ("1234", "1324")]
+    diff_swap = [("2134", "2143"), ("1234", "2134")]
+    verify_edges_have_the_same_color(tester, edge_24)
+    verify_edges_have_the_same_color(tester, edge_13)
+    verify_edges_have_the_same_color(tester, edge_23)
+    verify_edges_have_different_colors(tester, *diff_swap)
+
+
+def test_select_and_highlight_nodes():
+    # ============ Size 3 ============ #
+    visualizer = PosetVisualizer(3)
+    selected_nodes = ["123", "132", "312", "321"]
+    highlighted_nodes = ["123", "132", "312"]
+    visualizer.select_and_highlight_nodes(selected_nodes, highlighted_nodes)
+    tester = FigureTester(visualizer.get_figure_data())
+
+    # selected/highlighted nodes are part of the atg
+    # for this case, there are no "does not exist" nodes to check
+    selected_nodes = ["321"]
+    other_nodes = ["231", "213"]
+    highlighted_nodes = highlighted_nodes
+    verify_render_type_of_nodes(tester, selected_nodes, render_type="selected")
+    verify_render_type_of_nodes(tester, other_nodes, render_type="other")
+    verify_render_type_of_nodes(tester, highlighted_nodes, render_type="highlighted")
+
+    # selected edges are part of the atg, other edges are "visual aids"
+    selected_edges = [("312", "321")]
+    other_edges = [("321", "231"), ("231", "213"), ("213", "123")]
+    highlighted_edges = [("132", "312"), ("123", "132")]
+    some_edges_that_dont_exist = [
+        ("123", "312"),
+        ("123", "321"),
+        ("123", "231"),
+        ("312", "213"),
+    ]
+    verify_render_type_of_edges(tester, selected_edges, render_type="selected")
+    verify_render_type_of_edges(tester, other_edges, render_type="other")
+    verify_render_type_of_edges(tester, highlighted_edges, render_type="highlighted")
+    verify_edges_do_not_exist(tester, some_edges_that_dont_exist)
+
+    # edges are colored according to swap type except highlighted edges
+    # in this case, two out of three edges are highlighted, so nothing to test
+
+    # ============ Size 4 ============ #
+    visualizer = PosetVisualizer(4)
+    selected_nodes = TWOMAXIMAL
+    highlighted_nodes = ["4123", "1423", "1243", "1234"]
+    visualizer.select_and_highlight_nodes(selected_nodes, highlighted_nodes)
+    tester = FigureTester(visualizer.get_figure_data())
+
+    # selected/highlighted nodes are part of the atg
+    # for this case, there are no "does not exist" nodes to check
+    selected_nodes = list(set(TWOMAXIMAL) - set(highlighted_nodes))
+    some_other_nodes = ["4321", "4231", "2413"]
+    highlighted_nodes = highlighted_nodes
+    verify_render_type_of_nodes(tester, selected_nodes, render_type="selected")
+    verify_render_type_of_nodes(tester, some_other_nodes, render_type="other")
+    verify_render_type_of_nodes(tester, highlighted_nodes, render_type="highlighted")
+
+    # selected/highlighted edges are part of the atg, other edges are "visual aids"
+    some_selected_edges = [("2134", "2143"), ("1234", "1324"), ("3142", "3412")]
+    some_other_edges = [("4321", "4231"), ("2143", "2413")]
+    highlighted_edges = [("4123", "1423"), ("1423", "1243"), ("1243", "1234")]
+    some_edges_that_dont_exist = [("1243", "3142"), ("4123", "4321"), ("1234", "2314")]
+    verify_render_type_of_edges(tester, some_selected_edges, render_type="selected")
+    verify_render_type_of_edges(tester, some_other_edges, render_type="other")
+    verify_render_type_of_edges(tester, highlighted_edges, render_type="highlighted")
+    verify_edges_do_not_exist(tester, some_edges_that_dont_exist)
+
+    # edges are colored according to swap type except highlighted edges
+    edge_24 = [("3124", "3142"), ("1324", "1342")]
+    edge_13 = [("3124", "1324"), ("3142", "1342")]
+    edge_23 = [("1423", "1432"), ("1234", "1324")]
+    diff_swap = [("2134", "2143"), ("1234", "2134")]
+    verify_edges_have_the_same_color(tester, edge_24)
+    verify_edges_have_the_same_color(tester, edge_13)
+    verify_edges_have_the_same_color(tester, edge_23)
+    verify_edges_have_different_colors(tester, *diff_swap)
diff --git a/backend/tests/testing_utils.py b/backend/tests/testing_utils.py
index e6ffb2a..75b587a 100644
--- a/backend/tests/testing_utils.py
+++ b/backend/tests/testing_utils.py
@@ -17,24 +17,26 @@ def get_node_data(self, text: str) -> NodeData | None:
 
         for trace in self._data:
             is_node_trace = "markers" in trace.get("mode", "")
+            if not is_node_trace:
+                continue
 
-            if is_node_trace:
-                for i, node_text in enumerate(trace["text"]):
-                    if str(node_text) == text:
-                        return NodeData(
-                            hoverinfo=trace["hoverinfo"],
-                            marker=trace["marker"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            text=trace["text"][i],
-                            textposition=trace["textposition"],
-                            x=trace["x"][i],
-                            y=trace["y"][i],
-                            z=trace["z"][i],
-                        )
+            for i, node_text in enumerate(trace["text"]):
+                if str(node_text) != text:
+                    continue
 
+                return NodeData(
+                    hoverinfo=trace["hoverinfo"],
+                    marker=trace["marker"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    text=trace["text"][i],
+                    textposition=trace["textposition"],
+                    x=trace["x"][i],
+                    y=trace["y"][i],
+                    z=trace["z"][i],
+                )
         return None
 
     def get_edge_data(self, n1: str, n2: str) -> EdgeData | None:
@@ -42,35 +44,35 @@ def get_edge_data(self, n1: str, n2: str) -> EdgeData | None:
 
         for trace in self._data:
             is_edge_trace = "lines" in trace.get("mode", "")
+            if not is_edge_trace:
+                continue
 
-            if is_edge_trace:
-                for i in range(0, len(trace["x"]), 3):
-                    start_node = self.get_node_data_by_position(
-                        trace["x"][i], trace["y"][i], trace["z"][i]
-                    )
-                    end_node = self.get_node_data_by_position(
-                        trace["x"][i + 1], trace["y"][i + 1], trace["z"][i + 1]
-                    )
-
-                    if (
-                        start_node
-                        and end_node
-                        and (
-                            (start_node["text"] == n1 and end_node["text"] == n2)
-                            or (start_node["text"] == n2 and end_node["text"] == n1)
-                        )
-                    ):
-                        return EdgeData(
-                            hoverinfo=trace["hoverinfo"],
-                            line=trace["line"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            x=(trace["x"][i], trace["x"][i + 1]),
-                            y=(trace["y"][i], trace["x"][i + 1]),
-                            z=(trace["z"][i], trace["x"][i + 1]),
-                        )
+            for i in range(0, len(trace["x"]), 3):
+                start_node = self.get_node_data_by_position(
+                    trace["x"][i], trace["y"][i], trace["z"][i]
+                )
+                end_node = self.get_node_data_by_position(
+                    trace["x"][i + 1], trace["y"][i + 1], trace["z"][i + 1]
+                )
+
+                if not start_node or not end_node:
+                    continue
+                if not (start_node["text"] == n1 and end_node["text"] == n2) and not (
+                    start_node["text"] == n2 and end_node["text"] == n1
+                ):
+                    continue
+
+                return EdgeData(
+                    hoverinfo=trace["hoverinfo"],
+                    line=trace["line"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    x=(trace["x"][i], trace["x"][i + 1]),
+                    y=(trace["y"][i], trace["x"][i + 1]),
+                    z=(trace["z"][i], trace["x"][i + 1]),
+                )
         return None
 
     def get_node_data_by_position(
@@ -80,21 +82,24 @@ def get_node_data_by_position(
 
         for trace in self._data:
             is_node_trace = "markers" in trace.get("mode", "")
+            if not is_node_trace:
+                continue
+
+            for i in range(len(trace["text"])):
+                if (trace["x"][i], trace["y"][i], trace["z"][i]) != (x, y, z):
+                    continue
 
-            if is_node_trace:
-                for i in range(len(trace["text"])):
-                    if (trace["x"][i], trace["y"][i], trace["z"][i]) == (x, y, z):
-                        return NodeData(
-                            hoverinfo=trace["hoverinfo"],
-                            marker=trace["marker"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            text=trace["text"][i],
-                            textposition=trace["textposition"],
-                            x=trace["x"][i],
-                            y=trace["y"][i],
-                            z=trace["z"][i],
-                        )
+                return NodeData(
+                    hoverinfo=trace["hoverinfo"],
+                    marker=trace["marker"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    text=trace["text"][i],
+                    textposition=trace["textposition"],
+                    x=trace["x"][i],
+                    y=trace["y"][i],
+                    z=trace["z"][i],
+                )
         return None


Reviews:
user: login: rudnam
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-01T03:59:02Z
user: login: hcantonio1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-01T04:16:52Z
user: login: hcantonio1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-01T04:17:10Z
user: login: hcantonio1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-01T04:17:15Z
user: login: hcantonio1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-01T04:17:21Z
user: login: hcantonio1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-01T04:21:47Z
user: login: hcantonio1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-01T04:29:51Z
user: login: rudnam
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-01T07:06:51Z
user: login: rudnam
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-01T07:09:22Z

Review comments:
diff_hunk: @@ -1,10 +1,186 @@
+"""
+Test Overview
+
+Public interface of a PosetVisualizer (✓ - for testing; ✗ - maybe not)
+ - select_nodes                 ✓ function is useful
+ - highlight_nodes              ✗ not useful on its own
+ - select_and_highlight_nodes   ✓ function is useful
+ - update_figure                ✗ used only by the select node methods
+ - show_figure                  ✗ not used
+ - get_figure_data              ✓ main purpose of the visualizer
+
+Some notes regarding the purpose of the select-node functions
+    There are three classes of vertices and edges: selected, highlighted, and others
+    All vertices and edges start as "others"
+    select_nodes will move some vertices (and their edges) to "selected" class/status
+    highlight_nodes will move some vertices (and their edges) to "highlighted" class/status
+    select_and_highlight_nodes does both
+    Only then is get_figure called (see main.py)
+
+
+Charateristics of Nodes and Edges according to Class
+    +-------------------+-------------------+-------------------+
+    |       Class       |      Vertex       |       Edge        |
+    +-------------------+-------------------+-------------------+
+    |     Selected      |   color="black"   |color=<swap_color> |
+    |                   |      size=3       |      width=2      |
+    |                   |     opacity=1     |     opacity=1     |
+    +-------------------+-------------------+-------------------+
+    |    Highlighted    |    color="red"    |    color="red"    |
+    |                   |      size=6       |      width=5      |
+    |                   |     opacity=1     |     opacity=1     |
+    +-------------------+-------------------+-------------------+
+    |      Others       |   color="black"   |color=<swap_color> |
+    |                   |      size=3       |      width=2      |
+    |                   |    opacity=0.1    |    opacity=0.1    |
+    +-------------------+-------------------+-------------------+
+
+    Observations after investigating the traces.
+        Except for the variable color (<swap_color>), colors red and black can be verified as literals "red" and "black"
+        Then other attributes can be check for their numerical value
+
+Proposed Test Organization
+1. test "default" output (i.e. without selection; permutahedron). effectively the same as .select_nodes(<all_nodes>)
+    Call PosetVisualizer(n) for n = 3 to 5
+    a. edges
+    b. nodes
+2. test select_nodes
+    Call PosetVisualizer(n) for n = 3 and 4, then call .select_nodes()
+    a. edges
+    b. nodes
+3. test select_and_highlight_nodes()
+    Call PosetVisualizer(n) for n = 3 and 4, then call .select_and_highlight_nodes()
+    a. edges
+    b. nodes
+
+New Test Proposal
+Now, since it is clear we are operating with three render types, I suppose we should not test for the definition anymore.
+(Notice that the ATG in the paper does not deal with highlighted edges, nor "other" nodes.)
+Instead, we should simply test for the render types and hope that our representation still contains the ATG.
+1. test_errors_raised
+2. test "default" output (i.e. without selection; permutahedron). effectively the same as .select_nodes(<all_nodes>)
+    Call PosetVisualizer(n) for n = 3 and 4
+    a. nodes (selected)
+    b. edges (selected, does not exist)
+3. test select_nodes
+    Call PosetVisualizer(n) for n = 3 and 4, then call .select_nodes()
+    a. nodes (selected, other)
+    b. edges (selected, other, does not exist)
+4. test select_and_highlight_nodes()
+    Call PosetVisualizer(n) for n = 3 and 4, then call .select_and_highlight_nodes()
+    a. nodes (selected, highlighted, other)
+    b. edges (selected, highlighted, other, does not exist)
+"""
+
 import pytest
 
-from app.posetvisualizer import PosetVisualizer
+from app.posetvisualizer import PosetVisualizer, RenderType
 from tests.testing_utils import FigureTester
 
+from .upsilon_constants import TWOMAXIMAL
+
+RENDER_TYPE_SPECS = PosetVisualizer.RENDER_TYPE_SPECS
+
+
+def _verify_egdes_have_the_same_color(
path: backend/tests/test_posetvisualizer.py
user: login: rudnam
body: spelling
created_at: 2025-03-01T03:49:32Z
author_association: OWNER
diff_hunk: @@ -17,60 +17,62 @@ def get_node_data(self, text: str) -> NodeData | None:
 
         for trace in self._data:
             is_node_trace = "markers" in trace.get("mode", "")
+            if not is_node_trace:
+                continue
 
-            if is_node_trace:
-                for i, node_text in enumerate(trace["text"]):
-                    if str(node_text) == text:
-                        return NodeData(
-                            hoverinfo=trace["hoverinfo"],
-                            marker=trace["marker"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            text=trace["text"][i],
-                            textposition=trace["textposition"],
-                            x=trace["x"][i],
-                            y=trace["y"][i],
-                            z=trace["z"][i],
-                        )
+            for i, node_text in enumerate(trace["text"]):
+                if str(node_text) != text:
+                    continue
 
+                return NodeData(
+                    hoverinfo=trace["hoverinfo"],
+                    marker=trace["marker"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    text=trace["text"][i],
+                    textposition=trace["textposition"],
+                    x=trace["x"][i],
+                    y=trace["y"][i],
+                    z=trace["z"][i],
+                )
path: backend/tests/testing_utils.py
user: login: rudnam
body: why?
created_at: 2025-03-01T03:55:13Z
author_association: OWNER
diff_hunk: @@ -17,60 +17,62 @@ def get_node_data(self, text: str) -> NodeData | None:
 
         for trace in self._data:
             is_node_trace = "markers" in trace.get("mode", "")
+            if not is_node_trace:
+                continue
 
-            if is_node_trace:
-                for i, node_text in enumerate(trace["text"]):
-                    if str(node_text) == text:
-                        return NodeData(
-                            hoverinfo=trace["hoverinfo"],
-                            marker=trace["marker"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            text=trace["text"][i],
-                            textposition=trace["textposition"],
-                            x=trace["x"][i],
-                            y=trace["y"][i],
-                            z=trace["z"][i],
-                        )
+            for i, node_text in enumerate(trace["text"]):
+                if str(node_text) != text:
+                    continue
 
+                return NodeData(
+                    hoverinfo=trace["hoverinfo"],
+                    marker=trace["marker"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    text=trace["text"][i],
+                    textposition=trace["textposition"],
+                    x=trace["x"][i],
+                    y=trace["y"][i],
+                    z=trace["z"][i],
+                )
         return None
 
     def get_edge_data(self, n1: str, n2: str) -> EdgeData | None:
         """Find an edge by the text of its nodes."""
 
         for trace in self._data:
             is_edge_trace = "lines" in trace.get("mode", "")
+            if not is_edge_trace:
+                continue
 
-            if is_edge_trace:
-                for i in range(0, len(trace["x"]), 3):
-                    start_node = self.get_node_data_by_position(
-                        trace["x"][i], trace["y"][i], trace["z"][i]
-                    )
-                    end_node = self.get_node_data_by_position(
-                        trace["x"][i + 1], trace["y"][i + 1], trace["z"][i + 1]
-                    )
-
-                    if (
-                        start_node
-                        and end_node
-                        and (
-                            (start_node["text"] == n1 and end_node["text"] == n2)
-                            or (start_node["text"] == n2 and end_node["text"] == n1)
-                        )
-                    ):
-                        return EdgeData(
-                            hoverinfo=trace["hoverinfo"],
-                            line=trace["line"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            x=(trace["x"][i], trace["x"][i + 1]),
-                            y=(trace["y"][i], trace["x"][i + 1]),
-                            z=(trace["z"][i], trace["x"][i + 1]),
-                        )
+            for i in range(0, len(trace["x"]), 3):
+                start_node = self.get_node_data_by_position(
+                    trace["x"][i], trace["y"][i], trace["z"][i]
+                )
+                end_node = self.get_node_data_by_position(
+                    trace["x"][i + 1], trace["y"][i + 1], trace["z"][i + 1]
+                )
+
+                if not start_node or not end_node:
+                    continue
+                if not (start_node["text"] == n1 and end_node["text"] == n2) and not (
+                    start_node["text"] == n2 and end_node["text"] == n1
+                ):
+                    continue
+
+                return EdgeData(
+                    hoverinfo=trace["hoverinfo"],
+                    line=trace["line"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    x=(trace["x"][i], trace["x"][i + 1]),
+                    y=(trace["y"][i], trace["x"][i + 1]),
+                    z=(trace["z"][i], trace["x"][i + 1]),
+                )
path: backend/tests/testing_utils.py
user: login: rudnam
body: why
created_at: 2025-03-01T03:55:51Z
author_association: OWNER
diff_hunk: @@ -80,21 +82,24 @@ def get_node_data_by_position(
 
         for trace in self._data:
             is_node_trace = "markers" in trace.get("mode", "")
+            if not is_node_trace:
+                continue
+
+            for i in range(len(trace["text"])):
+                if (trace["x"][i], trace["y"][i], trace["z"][i]) != (x, y, z):
+                    continue
 
-            if is_node_trace:
-                for i in range(len(trace["text"])):
-                    if (trace["x"][i], trace["y"][i], trace["z"][i]) == (x, y, z):
-                        return NodeData(
-                            hoverinfo=trace["hoverinfo"],
-                            marker=trace["marker"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            text=trace["text"][i],
-                            textposition=trace["textposition"],
-                            x=trace["x"][i],
-                            y=trace["y"][i],
-                            z=trace["z"][i],
-                        )
+                return NodeData(
+                    hoverinfo=trace["hoverinfo"],
+                    marker=trace["marker"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    text=trace["text"][i],
+                    textposition=trace["textposition"],
+                    x=trace["x"][i],
+                    y=trace["y"][i],
+                    z=trace["z"][i],
+                )
path: backend/tests/testing_utils.py
user: login: rudnam
body: why
created_at: 2025-03-01T03:56:00Z
author_association: OWNER
path: backend/tests/test_posetvisualizer.py
user: login: rudnam
body: why not have the _verify functions as methods to FigureTester instead if we're just going to pass the tester as a parameter anyway
created_at: 2025-03-01T03:58:51Z
author_association: OWNER
diff_hunk: @@ -1,10 +1,186 @@
+"""
+Test Overview
+
+Public interface of a PosetVisualizer (✓ - for testing; ✗ - maybe not)
+ - select_nodes                 ✓ function is useful
+ - highlight_nodes              ✗ not useful on its own
+ - select_and_highlight_nodes   ✓ function is useful
+ - update_figure                ✗ used only by the select node methods
+ - show_figure                  ✗ not used
+ - get_figure_data              ✓ main purpose of the visualizer
+
+Some notes regarding the purpose of the select-node functions
+    There are three classes of vertices and edges: selected, highlighted, and others
+    All vertices and edges start as "others"
+    select_nodes will move some vertices (and their edges) to "selected" class/status
+    highlight_nodes will move some vertices (and their edges) to "highlighted" class/status
+    select_and_highlight_nodes does both
+    Only then is get_figure called (see main.py)
+
+
+Charateristics of Nodes and Edges according to Class
+    +-------------------+-------------------+-------------------+
+    |       Class       |      Vertex       |       Edge        |
+    +-------------------+-------------------+-------------------+
+    |     Selected      |   color="black"   |color=<swap_color> |
+    |                   |      size=3       |      width=2      |
+    |                   |     opacity=1     |     opacity=1     |
+    +-------------------+-------------------+-------------------+
+    |    Highlighted    |    color="red"    |    color="red"    |
+    |                   |      size=6       |      width=5      |
+    |                   |     opacity=1     |     opacity=1     |
+    +-------------------+-------------------+-------------------+
+    |      Others       |   color="black"   |color=<swap_color> |
+    |                   |      size=3       |      width=2      |
+    |                   |    opacity=0.1    |    opacity=0.1    |
+    +-------------------+-------------------+-------------------+
+
+    Observations after investigating the traces.
+        Except for the variable color (<swap_color>), colors red and black can be verified as literals "red" and "black"
+        Then other attributes can be check for their numerical value
+
+Proposed Test Organization
+1. test "default" output (i.e. without selection; permutahedron). effectively the same as .select_nodes(<all_nodes>)
+    Call PosetVisualizer(n) for n = 3 to 5
+    a. edges
+    b. nodes
+2. test select_nodes
+    Call PosetVisualizer(n) for n = 3 and 4, then call .select_nodes()
+    a. edges
+    b. nodes
+3. test select_and_highlight_nodes()
+    Call PosetVisualizer(n) for n = 3 and 4, then call .select_and_highlight_nodes()
+    a. edges
+    b. nodes
+
+New Test Proposal
+Now, since it is clear we are operating with three render types, I suppose we should not test for the definition anymore.
+(Notice that the ATG in the paper does not deal with highlighted edges, nor "other" nodes.)
+Instead, we should simply test for the render types and hope that our representation still contains the ATG.
+1. test_errors_raised
+2. test "default" output (i.e. without selection; permutahedron). effectively the same as .select_nodes(<all_nodes>)
+    Call PosetVisualizer(n) for n = 3 and 4
+    a. nodes (selected)
+    b. edges (selected, does not exist)
+3. test select_nodes
+    Call PosetVisualizer(n) for n = 3 and 4, then call .select_nodes()
+    a. nodes (selected, other)
+    b. edges (selected, other, does not exist)
+4. test select_and_highlight_nodes()
+    Call PosetVisualizer(n) for n = 3 and 4, then call .select_and_highlight_nodes()
+    a. nodes (selected, highlighted, other)
+    b. edges (selected, highlighted, other, does not exist)
+"""
+
 import pytest
 
-from app.posetvisualizer import PosetVisualizer
+from app.posetvisualizer import PosetVisualizer, RenderType
 from tests.testing_utils import FigureTester
 
+from .upsilon_constants import TWOMAXIMAL
+
+RENDER_TYPE_SPECS = PosetVisualizer.RENDER_TYPE_SPECS
+
+
+def _verify_egdes_have_the_same_color(
path: backend/tests/test_posetvisualizer.py
user: login: hcantonio1
body: right
created_at: 2025-03-01T04:16:52Z
author_association: COLLABORATOR
diff_hunk: @@ -17,60 +17,62 @@ def get_node_data(self, text: str) -> NodeData | None:
 
         for trace in self._data:
             is_node_trace = "markers" in trace.get("mode", "")
+            if not is_node_trace:
+                continue
 
-            if is_node_trace:
-                for i, node_text in enumerate(trace["text"]):
-                    if str(node_text) == text:
-                        return NodeData(
-                            hoverinfo=trace["hoverinfo"],
-                            marker=trace["marker"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            text=trace["text"][i],
-                            textposition=trace["textposition"],
-                            x=trace["x"][i],
-                            y=trace["y"][i],
-                            z=trace["z"][i],
-                        )
+            for i, node_text in enumerate(trace["text"]):
+                if str(node_text) != text:
+                    continue
 
+                return NodeData(
+                    hoverinfo=trace["hoverinfo"],
+                    marker=trace["marker"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    text=trace["text"][i],
+                    textposition=trace["textposition"],
+                    x=trace["x"][i],
+                    y=trace["y"][i],
+                    z=trace["z"][i],
+                )
path: backend/tests/testing_utils.py
user: login: hcantonio1
body: increase readability by reducing indentations
created_at: 2025-03-01T04:17:10Z
author_association: COLLABORATOR
diff_hunk: @@ -17,60 +17,62 @@ def get_node_data(self, text: str) -> NodeData | None:
 
         for trace in self._data:
             is_node_trace = "markers" in trace.get("mode", "")
+            if not is_node_trace:
+                continue
 
-            if is_node_trace:
-                for i, node_text in enumerate(trace["text"]):
-                    if str(node_text) == text:
-                        return NodeData(
-                            hoverinfo=trace["hoverinfo"],
-                            marker=trace["marker"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            text=trace["text"][i],
-                            textposition=trace["textposition"],
-                            x=trace["x"][i],
-                            y=trace["y"][i],
-                            z=trace["z"][i],
-                        )
+            for i, node_text in enumerate(trace["text"]):
+                if str(node_text) != text:
+                    continue
 
+                return NodeData(
+                    hoverinfo=trace["hoverinfo"],
+                    marker=trace["marker"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    text=trace["text"][i],
+                    textposition=trace["textposition"],
+                    x=trace["x"][i],
+                    y=trace["y"][i],
+                    z=trace["z"][i],
+                )
         return None
 
     def get_edge_data(self, n1: str, n2: str) -> EdgeData | None:
         """Find an edge by the text of its nodes."""
 
         for trace in self._data:
             is_edge_trace = "lines" in trace.get("mode", "")
+            if not is_edge_trace:
+                continue
 
-            if is_edge_trace:
-                for i in range(0, len(trace["x"]), 3):
-                    start_node = self.get_node_data_by_position(
-                        trace["x"][i], trace["y"][i], trace["z"][i]
-                    )
-                    end_node = self.get_node_data_by_position(
-                        trace["x"][i + 1], trace["y"][i + 1], trace["z"][i + 1]
-                    )
-
-                    if (
-                        start_node
-                        and end_node
-                        and (
-                            (start_node["text"] == n1 and end_node["text"] == n2)
-                            or (start_node["text"] == n2 and end_node["text"] == n1)
-                        )
-                    ):
-                        return EdgeData(
-                            hoverinfo=trace["hoverinfo"],
-                            line=trace["line"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            x=(trace["x"][i], trace["x"][i + 1]),
-                            y=(trace["y"][i], trace["x"][i + 1]),
-                            z=(trace["z"][i], trace["x"][i + 1]),
-                        )
+            for i in range(0, len(trace["x"]), 3):
+                start_node = self.get_node_data_by_position(
+                    trace["x"][i], trace["y"][i], trace["z"][i]
+                )
+                end_node = self.get_node_data_by_position(
+                    trace["x"][i + 1], trace["y"][i + 1], trace["z"][i + 1]
+                )
+
+                if not start_node or not end_node:
+                    continue
+                if not (start_node["text"] == n1 and end_node["text"] == n2) and not (
+                    start_node["text"] == n2 and end_node["text"] == n1
+                ):
+                    continue
+
+                return EdgeData(
+                    hoverinfo=trace["hoverinfo"],
+                    line=trace["line"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    x=(trace["x"][i], trace["x"][i + 1]),
+                    y=(trace["y"][i], trace["x"][i + 1]),
+                    z=(trace["z"][i], trace["x"][i + 1]),
+                )
path: backend/tests/testing_utils.py
user: login: hcantonio1
body: increase readability by reducing indentations
created_at: 2025-03-01T04:17:15Z
author_association: COLLABORATOR
diff_hunk: @@ -80,21 +82,24 @@ def get_node_data_by_position(
 
         for trace in self._data:
             is_node_trace = "markers" in trace.get("mode", "")
+            if not is_node_trace:
+                continue
+
+            for i in range(len(trace["text"])):
+                if (trace["x"][i], trace["y"][i], trace["z"][i]) != (x, y, z):
+                    continue
 
-            if is_node_trace:
-                for i in range(len(trace["text"])):
-                    if (trace["x"][i], trace["y"][i], trace["z"][i]) == (x, y, z):
-                        return NodeData(
-                            hoverinfo=trace["hoverinfo"],
-                            marker=trace["marker"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            text=trace["text"][i],
-                            textposition=trace["textposition"],
-                            x=trace["x"][i],
-                            y=trace["y"][i],
-                            z=trace["z"][i],
-                        )
+                return NodeData(
+                    hoverinfo=trace["hoverinfo"],
+                    marker=trace["marker"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    text=trace["text"][i],
+                    textposition=trace["textposition"],
+                    x=trace["x"][i],
+                    y=trace["y"][i],
+                    z=trace["z"][i],
+                )
path: backend/tests/testing_utils.py
user: login: hcantonio1
body: increase readability by reducing indentations
created_at: 2025-03-01T04:17:21Z
author_association: COLLABORATOR
path: backend/tests/test_posetvisualizer.py
user: login: hcantonio1
body: I was wary about "declaring" the tests in a separate file.

Parang...
The file name is test_posetvisualizer.py, but the tests are in testing_utils.py.

I want to keep the _verify methods in the actual test file, and this will be helpful because I am cosidering to use assert statements in them.
created_at: 2025-03-01T04:21:47Z
author_association: COLLABORATOR
diff_hunk: @@ -17,60 +17,62 @@ def get_node_data(self, text: str) -> NodeData | None:
 
         for trace in self._data:
             is_node_trace = "markers" in trace.get("mode", "")
+            if not is_node_trace:
+                continue
 
-            if is_node_trace:
-                for i, node_text in enumerate(trace["text"]):
-                    if str(node_text) == text:
-                        return NodeData(
-                            hoverinfo=trace["hoverinfo"],
-                            marker=trace["marker"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            text=trace["text"][i],
-                            textposition=trace["textposition"],
-                            x=trace["x"][i],
-                            y=trace["y"][i],
-                            z=trace["z"][i],
-                        )
+            for i, node_text in enumerate(trace["text"]):
+                if str(node_text) != text:
+                    continue
 
+                return NodeData(
+                    hoverinfo=trace["hoverinfo"],
+                    marker=trace["marker"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    text=trace["text"][i],
+                    textposition=trace["textposition"],
+                    x=trace["x"][i],
+                    y=trace["y"][i],
+                    z=trace["z"][i],
+                )
path: backend/tests/testing_utils.py
user: login: hcantonio1
body: Sorry I should provide supporting opinion
[Guard Clauses](https://medium.com/@scadge/if-statements-design-guard-clauses-might-be-all-you-need-67219a1a981a)
created_at: 2025-03-01T04:29:51Z
author_association: COLLABORATOR
diff_hunk: @@ -17,60 +17,62 @@ def get_node_data(self, text: str) -> NodeData | None:
 
         for trace in self._data:
             is_node_trace = "markers" in trace.get("mode", "")
+            if not is_node_trace:
+                continue
 
-            if is_node_trace:
-                for i, node_text in enumerate(trace["text"]):
-                    if str(node_text) == text:
-                        return NodeData(
-                            hoverinfo=trace["hoverinfo"],
-                            marker=trace["marker"],
-                            mode=trace["mode"],
-                            name=trace["name"],
-                            opacity=trace["opacity"],
-                            showlegend=trace["showlegend"],
-                            text=trace["text"][i],
-                            textposition=trace["textposition"],
-                            x=trace["x"][i],
-                            y=trace["y"][i],
-                            z=trace["z"][i],
-                        )
+            for i, node_text in enumerate(trace["text"]):
+                if str(node_text) != text:
+                    continue
 
+                return NodeData(
+                    hoverinfo=trace["hoverinfo"],
+                    marker=trace["marker"],
+                    mode=trace["mode"],
+                    name=trace["name"],
+                    opacity=trace["opacity"],
+                    showlegend=trace["showlegend"],
+                    text=trace["text"][i],
+                    textposition=trace["textposition"],
+                    x=trace["x"][i],
+                    y=trace["y"][i],
+                    z=trace["z"][i],
+                )
path: backend/tests/testing_utils.py
user: login: rudnam
body: ok!
created_at: 2025-03-01T07:06:51Z
author_association: OWNER
path: backend/tests/test_posetvisualizer.py
user: login: rudnam
body: i see. if you end up not using assert statements by the end baka better to make it into a FigureTester method nalang
created_at: 2025-03-01T07:09:22Z
author_association: OWNER

PR comments:
user: login: vercel[bot]
created_at: 2025-02-28T13:42:31Z
body: [vc]: #RgpQsQMgWHyPwCokMD5m45UL4vQRAb0TqtQDXtkUVWY=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJhdGctdmlzdWFsaXplciIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9ydWRuYW1zLXByb2plY3RzL2F0Zy12aXN1YWxpemVyLzZwUlZWdmtXV1lOckR6YWV2RUxMNDg2cTR6b2giLCJwcmV2aWV3VXJsIjoiYXRnLXZpc3VhbGl6ZXItZ2l0LXRlc3QtcG9zZXR2aXN1YWxpemVyLXJ1ZG5hbXMtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsInJvb3REaXJlY3RvcnkiOiJmcm9udGVuZCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **atg-visualizer** | ✅ Ready ([Inspect](https://vercel.com/rudnams-projects/atg-visualizer/6pRVVvkWWYNrDzaevELL486q4zoh)) | [Visit Preview](https://atg-visualizer-git-test-posetvisualizer-rudnams-projects.vercel.app) | Mar 3, 2025 2:18am |


author_association: NONE
user: login: hcantonio1
created_at: 2025-03-01T02:48:30Z
body: Reasoning for the TODO; context
- The "underscore" functions (of the form "\_verify\_*") operate on a given group of edges or nodes. For example, one checks if all edges have the same color, another check if all given nodes are highlighted.
- In the current implementation of test_posetvisualizer.py, it is not possible to pinpoint which edge or node violated the expectation.

Example: assert _verify_edges_have_same_color(tester, edges)
pytest will inform us that an assert False happened in some line with the arguments FigureTester object and a list of edges. So, we don't know which edge is differently colored.

Possible solutions
1. By moving the assertion into the underscore function, pytest will can tell us the exact edge or node that differed (in addition to us giving a "message" argument to the assert statement). The only issue here are the statements "assert not _verify_edges_have_same_color(tester, edges)", which are written by me. I cannot use the verify function to verify both "same-coloredness" and "different-coloredness". This can be addressed by creating a function for each expectation.
2. Make the "underscore" functions return a "reason" for the False return, e.g. tuple(False, reason). In this case we will know what happened, but it will cost extra lines in the "test_*" function, one for destructuring the tuple and next is the assert, which can possibly make the code unreadable.

EDIT:
After writing this, I realized **the first solution** is simpler and **is the better option.**

---
Sorry Im playing with markdown[^1]
==highlighted text==

[^1]: wow footnote
author_association: COLLABORATOR
user: login: rudnam
created_at: 2025-03-01T04:03:42Z
body: > Reasoning for the TODO; context
> 
> * The "underscore" functions (of the form "_verify_*") operate on a given group of edges or nodes. For example, one checks if all edges have the same color, another check if all given nodes are highlighted.
> * In the current implementation of test_posetvisualizer.py, it is not possible to pinpoint which edge or node violated the expectation.
> 
> Example: assert _verify_edges_have_same_color(tester, edges) pytest will inform us that an assert False happened in some line with the arguments FigureTester object and a list of edges. So, we don't know which edge is differently colored.
> 
> Possible solutions
> 
> 1. By moving the assertion into the underscore function, pytest will can tell us the exact edge or node that differed (in addition to us giving a "message" argument to the assert statement). The only issue here are the statements "assert not _verify_edges_have_same_color(tester, edges)", which are written by me. I cannot use the verify function to verify both "same-coloredness" and "different-coloredness". This can be addressed by creating a function for each expectation.
> 2. Make the "underscore" functions return a "reason" for the False return, e.g. tuple(False, reason). In this case we will know what happened, but it will cost extra lines in the "test_*" function, one for destructuring the tuple and next is the assert, which can possibly make the code unreadable.
> 
> EDIT: After writing this, I realized **the first solution** is simpler and **is the better option.**
> 
> Sorry Im playing with markdown[1](#user-content-fn-1-4b98c4a2a3bf4f2849281f87c8b78798) ==highlighted text==
> 
> ## Footnotes
> 1. wow footnote [↩](#user-content-fnref-1-4b98c4a2a3bf4f2849281f87c8b78798)

I think it should be enough for this case that an error can be detected and further details can just be checked manually
author_association: OWNER
user: login: rudnam
created_at: 2025-03-01T07:09:54Z
body: Closes #9 
author_association: OWNER
user: login: rudnam
created_at: 2025-03-02T14:30:31Z
body: Can you remove the leading underscore rin from _verify
author_association: OWNER