Pages

Now that the backend is complete, we can create the GUI elements. We build our own implementation of the Scenarios page, the Compare page, and the Overview page. Algomancy uses Plotly Dash for the GUI; in this tutorial we use components from Dash Core Components and Dash Bootstrap Components.

The quickstart generated skeleton page classes in src/pages/:

  • scenario_page.pyTSPScenarioPage

  • compare_page.pyTSPComparePage

  • overview_page.pyTSPOverviewPage

These are already wired into main.py. We implement them one by one below.

Scenarios Page

On the Scenarios page we show basic information about the scenario, and — once the algorithm has run — result statistics and a route visualisation.

  1. Open src/pages/scenario_page.py. The quickstart generated a TSPScenarioPage skeleton. Replace its create_content and register_callbacks methods with the implementations below.

  2. We will share several visualisation components across pages. Create components.py in src/pages/:

Code

Tip

The getattr function (see, e.g., line 23 below) provides safe attribute access for objects that may not yet carry results. This is considered good practice when the object’s state is not guaranteed at render time.

components.py
 1from dash import html
 2import dash_bootstrap_components as dbc
 3from algomancy_gui.scenario_page.scenario_badge import status_badge
 4from typing import List, Dict
 5
 6
 7def scenario_table(scenario) -> html.Div:
 8    """
 9    Render a summary table with high-level scenario metadata.
10
11    Inputs / assumptions:
12    - scenario has the following attributes:
13        - id
14        - tag
15        - status
16        - algorithm_description
17        - input_data_key
18    - Missing attributes are rendered as an em dash ("—")
19
20    Output:
21    - html.Div containing a Bootstrap card with a metadata table
22    """
23    rows = [
24        html.Tr([html.Th("ID"), html.Td(str(getattr(scenario, "id", "—")))]),
25        html.Tr([html.Th("Tag"), html.Td(getattr(scenario, "tag", "—") or "—")]),
26        html.Tr([
27            html.Th("Status"),
28            html.Td(
29                status_badge(getattr(scenario, "status", "—"))
30            )
31        ]),
32        html.Tr([html.Th("Algorithm"),
33                 html.Td(getattr(scenario, "algorithm_description", "—") or "—")]),
34        html.Tr([html.Th("Dataset"),
35                 html.Td(getattr(scenario, "input_data_key", "—") or "—")]),
36    ]
37
38    return html.Div(dbc.Card(
39        [
40            dbc.CardHeader("Selected Scenario"),
41            dbc.CardBody(
42                dbc.Table(
43                    [html.Tbody(rows)],
44                )
45            ),
46        ]
47    ))
48
49def result_table(scenario) -> html.Div:
50    """
51    Render a KPI summary table for a completed scenario.
52
53    Inputs / assumptions:
54    - scenario.result contains:
55        - ordered_locations (list)
56        - tour (list)
57    - scenario.kpis contains:
58        - "Total_costs" with attribute `.value`
59    - Missing data is rendered as zero or em dash
60
61    Output:
62    - html.Div containing a Bootstrap card with result KPIs
63    """
64    ordered_locations = getattr(getattr(scenario, "result", None), "ordered_locations", []) or []
65    tour = getattr(getattr(scenario, "result", None), "tour", []) or []
66
67    total_cost = round(getattr(scenario.kpis.get("Total_costs", None), "value", 0.0), 1) if getattr(scenario, "kpis", None) else 0.0
68    number_of_routes = len(tour)
69    avg_cost = round(total_cost / number_of_routes, 1) if number_of_routes > 0 else 0.0
70
71    rows = [
72        html.Tr([html.Th("Total cost"), html.Td(f"{total_cost}")]),
73        html.Tr([html.Th("Number of locations"), html.Td(f"{len(ordered_locations)}")]),
74        html.Tr([html.Th("Number of route segments"), html.Td(f"{number_of_routes}")]),
75        html.Tr([html.Th("Average cost per segment"), html.Td(f"{avg_cost}")]),
76    ]
77
78    return html.Div(
79        dbc.Card(
80            [
81                dbc.CardHeader("Result Summary"),
82                dbc.CardBody(
83                    dbc.Table(
84                        [html.Tbody(rows)]
85                    )
86                ),
87            ]
88        )
89    )

Note that we re-use Algomancy’s status_badge component.

  1. In scenario_page.py, import these components and implement the create_content function:

Code
scenario_page.py
12@staticmethod
13def create_content(scenario: Scenario) -> html.Div:
14    # Case 1 – Scenario not ready
15    if scenario.status != ScenarioStatus.COMPLETE:
16        unavailable_page = dbc.Container([dbc.Row(
17            [
18                dbc.Col(scenario_table(scenario)),
19                dbc.Col(html.Div([
20                    dbc.Alert("Scenario results are not available yet. Please run the scenario or refresh "
21                              "the page once the computation is complete.",
22                              color="info")
23                ],
24                    style={"paddingTop": "4px"})
25                )
26            ]
27        )
28        ])
29        return html.Div(unavailable_page)
30
31    # Case 2 – Scenario finished: extract results
32    ordered_locations = scenario.result.ordered_locations
33    tour = scenario.result.tour
34
35    locations_payload = [
36        {"id": loc.id, "x": float(loc.x), "y": float(loc.y)}
37        for loc in ordered_locations
38    ]
39    tour_payload = [
40        {
41            "from_id": r.from_id,
42            "to_id": r.to_id,
43            "route_id": r.route_id,
44            "cost": float(getattr(r, "cost", 0.0)),
45        }
46        for r in tour
47    ]
48
49    # Construct page
50    result_page = dbc.Container([
51        dbc.Row(
52            [
53                dbc.Col(
54                    scenario_table(scenario)
55                    ),
56                dbc.Col(
57                    result_table(scenario)
58                ))
59            ])
60    ])
61    return html.Div(result_page)

In the created page, we check if the scenario has already run. If yes, we show the results; if not, we show a notification.

  1. Next, add a route visualisation component. In components.py, add:

import plotly.graph_objects as go

and add the following function:

Code
components.py
 94def route_visualization(ordered_locations: List[Dict], tour: List[Dict]):
 95    """
 96    Create a Plotly figure visualizing a TSP route.
 97
 98    Inputs / assumptions:
 99    - ordered_locations: list of dicts with keys:
100        - "id", "x", "y"
101    - tour: list of dicts with keys:
102        - "from_id", "to_id", "route_id", "cost"
103    - Location IDs referenced in tour exist in ordered_locations
104      (missing IDs should be handled or skipped by the caller)
105
106    Output:
107    - plotly.graph_objects.Figure showing route segments and locations
108    """
109    # Lookup table for coordinates
110    loc_lookup = {loc['id']: (loc['x'], loc['y']) for loc in ordered_locations}
111
112    xs = [loc['x'] for loc in ordered_locations]
113    ys = [loc['y'] for loc in ordered_locations]
114
115    if not xs or not ys:
116        x_min = y_min = 0.0
117        x_max = y_max = 1.0
118    else:
119        x_min, x_max = min(xs), max(xs)
120        y_min, y_max = min(ys), max(ys)
121
122    x_span = max(1e-9, x_max - x_min)
123    y_span = max(1e-9, y_max - y_min)
124
125    fig = go.Figure()
126    LINE_COLOR = "#0074D9"
127
128    for route in tour:
129        try:
130            x0, y0 = loc_lookup[route["from_id"]]
131            x1, y1 = loc_lookup[route["to_id"]]
132        except KeyError:
133            continue
134
135        fig.add_trace(
136            go.Scatter(
137                x=[x0, x1],
138                y=[y0, y1],
139                mode="lines",
140                line=dict(width=3, color=LINE_COLOR),
141                showlegend=False,
142            )
143        )
144
145        xm = (x0 + x1) / 2.0
146        ym = (y0 + y1) / 2.0
147
148        fig.add_annotation(
149            x=xm,
150            y=ym,
151            text=f"{route['route_id']}<br>Cost: {route['cost']:.1f}",
152            showarrow=False,
153            font=dict(size=11, color="#222"),
154            align="center",
155            xanchor="center",
156            yanchor="middle",
157            bgcolor="rgba(255,255,255,0.65)",
158            bordercolor="rgba(0,0,0,0.15)",
159            borderwidth=1,
160            borderpad=3,
161        )
162
163    fig.add_trace(
164        go.Scatter(
165            x=xs,
166            y=ys,
167            mode="markers",
168            marker=dict(size=9, color="#111111"),
169            text=[loc['id'] for loc in ordered_locations],
170            hovertemplate="<b>Location:</b> %{text}<extra></extra>",
171            showlegend=False,
172        )
173    )
174
175    fig.update_layout(
176        xaxis_title="X",
177        yaxis_title="Y",
178        hovermode="closest",
179        paper_bgcolor="rgba(0,0,0,0)",
180        plot_bgcolor="rgba(0,0,0,0)"
181    )
182
183    x_pad = 0.05 * x_span
184    y_pad = 0.05 * y_span
185    fig.update_xaxes(range=[x_min - x_pad, x_max + x_pad], zeroline=True, showgrid=True)
186    fig.update_yaxes(range=[y_min - y_pad, y_max + y_pad], zeroline=True, showgrid=True)
187
188    return fig
  1. In scenario_page.py, import route_visualization from components.py and update create_content to add a toggle for the visualisation:

Code
scenario_page.py
 12def create_content(scenario: Scenario) -> html.Div:
 13    """
 14    Create the main content of the scenario page.
 15
 16    Renders basic scenario metadata and either a notification (if the scenario is not complete) or result statistics
 17    and visualization controls.
 18
 19    Inputs / assumptions:
 20    - scenario has attributes:
 21        - status
 22        - result.ordered_locations
 23        - result.tour
 24        - kpis (optional)
 25    - ScenarioStatus.COMPLETE indicates a finished scenario
 26
 27    Output:
 28    - html.Div containing the full page layout for the scenario
 29    """
 30    # Case 1 – Scenario not ready
 31    if scenario.status != ScenarioStatus.COMPLETE:
 32        unavailable_page = dbc.Container([dbc.Row(
 33            [
 34                dbc.Col(scenario_table(scenario)),
 35                dbc.Col(html.Div([
 36                    dbc.Alert("Scenario results are not available yet. Please run the scenario or refresh "
 37                              "the page once the computation is complete.",
 38                              color="info")
 39                ],
 40                    style={"paddingTop": "4px"})
 41                )
 42            ]
 43        )
 44        ])
 45        return html.Div(unavailable_page)
 46
 47    # Case 2 – Scenario finished: extract results
 48    ordered_locations = scenario.result.ordered_locations
 49    tour = scenario.result.tour
 50
 51    locations_payload = [
 52        {"id": loc.id, "x": float(loc.x), "y": float(loc.y)}
 53        for loc in ordered_locations
 54    ]
 55    tour_payload = [
 56        {
 57            "from_id": r.from_id,
 58            "to_id": r.to_id,
 59            "route_id": r.route_id,
 60            "cost": float(getattr(r, "cost", 0.0)),
 61        }
 62        for r in tour
 63    ]
 64
 65    # Construct page
 66    result_page = dbc.Container([
 67        dbc.Row(
 68            [
 69                dbc.Col(scenario_table(scenario)),
 70                dbc.Col(html.Div([
 71                    # Key statistics
 72                    result_table(scenario),
 73
 74                    # Toggle visualization on/off
 75                    dbc.Checklist(
 76                        id="show-route",
 77                        options=[{"label": " Show visualization", "value": "show"}],
 78                        value=["show"],  # default: visible
 79                        switch=True,
 80                        style={"marginTop": "8px", "marginBottom": "8px"},
 81                    )
 82                ]))
 83            ]),
 84        dbc.Row(
 85            [
 86                html.Div([
 87                    html.Hr(),
 88                    dcc.Store(
 89                        id='locations_store', data=locations_payload
 90                    ),
 91                    dcc.Store(
 92                        id='tour_store', data=tour_payload
 93                    ),
 94                    html.Div(id="route-container")
 95                ]
 96                )
 97            ]
 98        )
 99    ])
100    return html.Div(result_page)
  1. Implement register_callbacks to respond to the visualisation toggle:

Code
scenario_page.py
10@staticmethod
11def register_callbacks():
12    """
13    Conditionally render the route visualization based on user input.
14
15    Inputs / assumptions:
16    - values: list of selected values from the 'show-route' checklist
17    - ordered_locations: data from dcc.Store, passed to route_visualization
18    - tour: data from dcc.Store, passed to route_visualization
19
20    Output:
21    - html.Div with a message if hidden, or
22    - dcc.Graph containing the route visualization
23    """
24    @callback(
25        Output("route-container", "children"),
26        Input("show-route", "value"),
27        State("locations_store", "data"),
28        State("tour_store", "data"),
29    )
30    def render_route(values, ordered_locations, tour):
31        show = "show" in (values or [])
32        if not show:
33            return html.Div("Visualization is hidden.", style={"opacity": 0.7, "fontStyle": "italic"})
34        return dcc.Graph(id="route", figure=route_visualization(ordered_locations, tour),
35                         style={"height": "58vh"})
  1. Start the application, load the data, and create a new scenario on the Scenarios page. Verify that the page you just implemented is shown.

Compare Page

On the Compare page we can do a more detailed side-by-side comparison of two scenarios.

  1. Open src/pages/compare_page.py. Implement create_side_by_side_content, create_compare_section, create_details_section, and register_callbacks:

Code
compare_page.py
 1from algomancy_scenario import Scenario
 2from algomancy_gui.page import BaseComparePage
 3from dash import html
 4import dash_bootstrap_components as dbc
 5from pages.components import result_table
 6
 7class TSPComparePage(BaseComparePage):
 8    @staticmethod
 9    def create_side_by_side_content(scenario: Scenario, side: str) -> html.Div:
10        """
11        Create the per-scenario content for the compare page.
12
13        Inputs / assumptions:
14        - scenario is a Scenario instance
15        - side indicates whether this is the left or right comparison side
16          (not used directly in this implementation)
17
18        Output:
19        - html.Div containing a result summary table for the scenario
20        """
21        results = result_table(scenario)
22        return html.Div(dbc.Container([results]))
23
24    @staticmethod
25    def create_compare_section(left: Scenario, right: Scenario) -> html.Div:
26        pass
27
28
29    @staticmethod
30    def create_details_section(left: Scenario, right: Scenario) -> html.Div:
31        pass
32
33    @staticmethod
34    def register_callbacks() -> None:
35        return None

Note that we re-use the result_table component from the Scenarios page.

  1. Implement create_compare_section and create_details_section with TSP-specific comparison logic:

Code
compare_page.py
 1    @staticmethod
 2    def create_compare_section(left: Scenario, right: Scenario) -> html.Div:
 3        """
 4        Create a comparison summary between two completed scenarios.
 5
 6        Computes simple similarities and differences between scenario results,
 7        including overlapping route segments and the most expensive route used.
 8
 9        Inputs / assumptions:
10        - left and right are Scenario instances with existing tours
11        - Route IDs uniquely identify comparable route segments
12
13        Output:
14        - html.Div containing textual comparison metrics
15        """
16        tour_left = getattr(getattr(left, "result", None), "tour", []) or []
17        tour_right = getattr(getattr(right, "result", None), "tour", []) or []
18
19        common_route_ids = {r.route_id for r in tour_left} & {r.route_id for r in tour_right}
20        number_of_common_routes = len(common_route_ids)
21
22        all_routes = tour_left + tour_right
23        if all_routes:
24            highest = max(all_routes, key=lambda r: r.cost)
25            highest_cost_id = highest.route_id
26            highest_cost = highest.cost
27        else:
28            highest_cost_id = ''
29            highest_cost = 0
30
31        left_ids = {r.route_id for r in tour_left}
32        right_ids = {r.route_id for r in tour_right}
33
34        used_in = (
35            "both" if (highest_cost_id in left_ids and highest_cost_id in right_ids)
36            else "left only" if (highest_cost_id in left_ids)
37            else "right only"
38        )
39
40        return html.Div([
41            html.Div(f"Number of common route segments: {number_of_common_routes}"),
42            html.Div(f"Highest cost route is {highest_cost_id} at ${highest_cost:.1f}, used in {used_in}")
43        ],
44            style={"marginTop": "8px"},
45        )
46
47
48    @staticmethod
49    def create_details_section(left: Scenario, right: Scenario) -> html.Div:
50        """
51        Create a simple details section displaying identifiers of both scenarios.
52
53        Inputs / assumptions:
54        - left and right are Scenario instances
55        - Each scenario exposes an 'id' attribute
56
57        Output:
58        - html.Div containing basic scenario identification details
59        """
60        return html.Div([
61            html.Div(f"Left scenario id: {left.id}"),
62            html.Div(f"Right scenario id: {right.id}"),
63        ],
64            style={"marginTop": "8px"},
65        )
  1. If desired, add interactivity by implementing the register_callbacks function, in the same way as on the Scenarios page.

Overview Page

On the Overview page we have access to all scenarios at once. For the TSP Overview page, we create a table with all scenarios, showing basic info and total cost for each.

  1. Open src/pages/overview_page.py. Implement create_content and register_callbacks:

Code
overview_page.py
 1from algomancy_scenario import Scenario
 2from algomancy_scenario import ScenarioStatus
 3from algomancy_gui.page import BaseOverviewPage
 4from algomancy_gui.scenario_page.scenario_badge import status_badge
 5
 6from typing import List
 7import dash_bootstrap_components as dbc
 8from dash import html
 9
10class TSPOverviewPage(BaseOverviewPage):
11    @staticmethod
12    def create_content(scenarios: List[Scenario]) -> html.Div:
13        """
14        Create an overview table summarizing multiple scenarios.
15
16        Inputs / assumptions:
17        - scenarios is a list of Scenario objects
18        - Each scenario defines:
19            - tag
20            - status
21            - input_data_key
22            - algorithm_description
23            - kpis["Total_costs"].value (only if COMPLETE)
24
25        Output:
26        - html.Div containing a Bootstrap card with a summary table, or an alert message if no scenarios are provided
27        """
28        # Empty-state
29        if not scenarios:
30            return html.Div(
31                dbc.Alert("No scenarios to display.", color="info")
32            )
33
34        header = html.Thead(
35            html.Tr(
36                [
37                    html.Th("Scenario"),
38                    html.Th("Status"),
39                    html.Th("Dataset"),
40                    html.Th("Algorithm",
41                    html.Th("Total cost"),
42                ]
43            )
44        )
45
46        body_rows = []
47        for s in scenarios:
48            tag = getattr(s, "tag", None)
49            status = getattr(s, "status", "—")
50            dataset = getattr(s, "input_data_key", None)
51            algo = getattr(s, "algorithm_description", None)
52
53            if (status == ScenarioStatus.COMPLETE) and (getattr(s, "kpis", None)):
54                cost = getattr(s.kpis.get("Total_costs", None), "value", 0.0)
55                cost_txt = f"{cost:.1f}"
56            else:
57                cost_txt = "—"
58
59            body_rows.append(
60                html.Tr(
61                    [
62                        html.Td(tag),
63                        html.Td(status_badge(status)),
64                        html.Td(dataset),
65                        html.Td(algo),
66                        html.Td(cost_txt),
67                    ]
68                )
69            )
70
71        table = dbc.Table(
72            [header, html.Tbody(body_rows)],
73        )
74
75        return html.Div(
76            dbc.Card(
77                [
78                    dbc.CardHeader("Scenarios"),
79                    dbc.CardBody(table),
80                ],
81                className="shadow-sm",
82            )
83        )
84
85    @staticmethod
86    def register_callbacks():
87        return None
  1. To enable the Overview page, uncomment the overview_page line in main.py:

overview_page=TSPOverviewPage(),
  1. If desired, add interactivity by implementing the register_callbacks function, in the same way as on the Scenarios page.

The created pages can be further customised to your preferences.