Plotly doesn't have an outline for their fonts so I had do improvise and place a bigger text underneath the original one.
This does not look the best, I could think about injecting HTML directly in a dash app (see chatgpt dump)
import dash
from dash import dcc, html
import plotly.graph_objects as go
from pathlib import Path
class SolutionPlotter:
COLOR_LIST = [
"blue", "red", "green", "purple", "orange", "yellow", "pink",
"brown", "grey", "black", "cyan", "magenta"
]
def __init__(self, num_vehicles, routes, locations, location_names, capacities, use_capacity, total_distance, depot, use_depot):
self.num_vehicles = num_vehicles
self.routes = routes
self.locations = locations
self.location_names = location_names
self.capacities = capacities
self.use_capacity = use_capacity
self.total_distance = total_distance
self.depot = depot
self.use_depot = use_depot
def display(self, file_name: str = None, results_path: str = "results"):
"""
Display the solution using a plotly figure.
Saves the figure to an HTML file if a file name is provided.
"""
fig = go.Figure()
for vehicle_id in range(self.num_vehicles):
route_coordinates = [
(self.locations[node][0], self.locations[node][1])
for node in self.routes[vehicle_id]
]
color = self.COLOR_LIST[vehicle_id % len(self.COLOR_LIST)]
capacity = (
self.capacities[vehicle_id]
if isinstance(self.capacities, list)
else self.capacities
)
legend_group = f"Vehicle {vehicle_id + 1}"
legend_name = (
f"Vehicle {vehicle_id + 1} ({capacity})"
if self.use_capacity
else f"Vehicle {vehicle_id + 1}"
)
# Draw routes
fig.add_trace(
go.Scatter(
x=[loc[0] for loc in route_coordinates],
y=[loc[1] for loc in route_coordinates],
mode="lines",
line=dict(width=5, color=color),
name=legend_name,
legendgroup=legend_group,
)
)
# Draw annotations
for i in range(len(route_coordinates) - 1):
loc_name = self.location_names[self.routes[vehicle_id][i]]
self.plot_direction(
fig,
route_coordinates[i],
route_coordinates[i + 1],
color,
5,
legend_group,
)
self.plot_location(
fig,
route_coordinates[i],
color,
loc_name,
legend_group,
vehicle_id,
i,
)
if not self.use_depot and len(route_coordinates) > 0:
self.plot_location(
fig,
route_coordinates[-1],
color,
self.location_names[self.routes[vehicle_id][-1]],
legend_group,
vehicle_id,
len(route_coordinates) - 1,
)
if self.use_depot:
self.plot_location(
fig, self.locations[self.depot], "gray", self.location_names[self.depot]
)
fig.update_layout(
xaxis_title="X Coordinate",
yaxis_title="Y Coordinate",
legend=dict(
title=f"Total Distance: {self.total_distance}m",
orientation="h",
yanchor="bottom",
y=1.02,
),
)
app = dash.Dash(__name__)
app.layout = html.Div(
[
dcc.Graph(id="scatter-plot", figure=fig),
# Add HTML styled text for each location
*[
html.Div(
loc_name,
style={
"position": "absolute",
"left": f"{loc[0]}px",
"top": f"{loc[1]}px",
"transform": "translate(-50%, -50%)",
"color": "white",
"font-size": "15px",
"text-shadow": "-1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000",
},
)
for loc, loc_name in zip(self.locations, self.location_names)
]
],
style={"position": "relative", "width": "100%", "height": "100%"},
)
app.run_server(debug=True)
if file_name is not None:
html_path = f"{results_path}/html"
Path(html_path).mkdir(parents=True, exist_ok=True)
fig.write_html(f"{html_path}/{file_name}.html")
def plot_direction(self, fig, loc1, loc2, color, line_width, legend_group=None):
"""
Plot an arrow representing the direction from coord1 to coord2 with the given color and line width.
"""
x_mid = (loc1[0] + loc2[0]) / 2
y_mid = (loc1[1] + loc2[1]) / 2
fig.add_trace(
go.Scatter(
x=[loc1[0], x_mid],
y=[loc1[1], y_mid],
mode="lines+markers",
line=dict(width=line_width, color=color),
marker=dict(size=20, symbol="arrow-up", angleref="previous"),
hoverinfo="skip",
showlegend=False,
legendgroup=legend_group,
)
)
def plot_location(
self,
fig,
loc,
color,
name,
legend_group=None,
vehicle_id=None,
route_id=None,
):
"""
Plot a location with the given color and legend group.
"""
hovertext = (
"Starting Point"
if vehicle_id is None
else (
f"Vehicle {vehicle_id + 1}: {self.loads[vehicle_id][route_id]} passengers"
if self.use_capacity
else f"Vehicle {vehicle_id + 1}"
)
)
fig.add_trace(
go.Scatter(
x=[loc[0]],
y=[loc[1]],
mode="markers+text",
marker=dict(size=50, symbol="circle", color=color, line_width=2),
text=name,
textposition="middle center",
textfont=dict(color="white", size=15),
showlegend=False,
hoverinfo="text",
hovertext=hovertext,
legendgroup=legend_group,
)
)
Plotly doesn't have an outline for their fonts so I had do improvise and place a bigger text underneath the original one. This does not look the best, I could think about injecting HTML directly in a dash app (see chatgpt dump)