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.py→TSPScenarioPagecompare_page.py→TSPComparePageoverview_page.py→TSPOverviewPage
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.
Open
src/pages/scenario_page.py. The quickstart generated aTSPScenarioPageskeleton. Replace itscreate_contentandregister_callbacksmethods with the implementations below.We will share several visualisation components across pages. Create
components.pyinsrc/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.
In
scenario_page.py, import these components and implement thecreate_contentfunction:
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.
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
In
scenario_page.py, importroute_visualizationfromcomponents.pyand updatecreate_contentto 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)
Implement
register_callbacksto 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"})
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.
Open
src/pages/compare_page.py. Implementcreate_side_by_side_content,create_compare_section,create_details_section, andregister_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.
Implement
create_compare_sectionandcreate_details_sectionwith 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 )
If desired, add interactivity by implementing the
register_callbacksfunction, 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.
Open
src/pages/overview_page.py. Implementcreate_contentandregister_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
To enable the Overview page, uncomment the
overview_pageline inmain.py:
overview_page=TSPOverviewPage(),
If desired, add interactivity by implementing the
register_callbacksfunction, in the same way as on the Scenarios page.
The created pages can be further customised to your preferences.