BrunoRosendo / master-thesis

Source code for my master's thesis, in the topic "Quantum algorithms for optimizing urban transportation"
MIT License
5 stars 0 forks source link

Improve outline of location names #96

Closed BrunoRosendo closed 1 week ago

BrunoRosendo commented 3 months ago

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,
            )
        )
BrunoRosendo commented 2 weeks ago

I think it is fine as is, changing into wontfixe

BrunoRosendo commented 1 week ago

migrated to https://github.com/BrunoRosendo/vrp-quantum-solver/issues