UKVI Travel History Visualizer (plotly)

Improving the visualisation of previous example using plotly.

Out:

Roundtrips:
   Outbound Date Inbound Date Outbound Ports Inbound Ports  ...  Voyage Code Departure Airport y_axis_str                                    Voyage Label
0     2019-12-04   2020-01-13        STN-MAD       SGN-LHR  ...       FR5998               STN         00   STN-MAD (04 Dec) → SGN-LHR (13 Jan) | 40 Days
1     2020-09-15   2021-02-08        STN-RMI       MAD-LHR  ...       FR6035               STN         01  STN-RMI (15 Sep) → MAD-LHR (08 Feb) | 146 Days
2     2021-06-17   2021-10-05        STN-MAD       MAD-STN  ...       FR5994               STN         02  STN-MAD (17 Jun) → MAD-STN (05 Oct) | 110 Days
3     2021-11-19   2021-12-02        STN-BLO       RMU-LGW  ...       FRO194               STN         03   STN-BLO (19 Nov) → RMU-LGW (02 Dec) | 13 Days
4     2022-01-20   2022-02-15        LGW-MAD       MAD-STN  ...       UX1016               LGW         04   LGW-MAD (20 Jan) → MAD-STN (15 Feb) | 26 Days
5     2022-03-24   2022-03-30        STN-TFS       TFS-LGW  ...       LS1663               STN         05    STN-TFS (24 Mar) → TFS-LGW (30 Mar) | 6 Days
6     2022-04-22   2022-04-27        STN-LIS       LIS-STN  ...       FR1886               STN         06    STN-LIS (22 Apr) → LIS-STN (27 Apr) | 5 Days
7     2022-06-03   2022-06-14        STN-MAD       MAD-STN  ...       FR5996               STN         07   STN-MAD (03 Jun) → MAD-STN (14 Jun) | 11 Days
8     2022-08-06   2022-09-21        LTN-CDT       MAD-STN  ...       W94495               LTN         08   LTN-CDT (06 Aug) → MAD-STN (21 Sep) | 46 Days
9     2022-10-26   2022-11-15        STN-MAD       MAD-LHR  ...       FR5996               STN         09   STN-MAD (26 Oct) → MAD-LHR (15 Nov) | 20 Days
10    2022-11-16   2022-12-26        LHR-KUL       KUL-LHR  ...       MHOOO1               LHR         10   LHR-KUL (16 Nov) → KUL-LHR (26 Dec) | 40 Days
11    2023-02-04   2023-02-06        LTN-ATH       ATH-LGW  ...       W94467               LTN         11    LTN-ATH (04 Feb) → ATH-LGW (06 Feb) | 2 Days
12    2023-02-21   2023-03-01        LTN-BLQ       PEG-STN  ...       FR3406               LTN         12    LTN-BLQ (21 Feb) → PEG-STN (01 Mar) | 8 Days
13    2023-04-28   2023-05-09        STN-LPA       LPA-STN  ...       FR2842               STN         13   STN-LPA (28 Apr) → LPA-STN (09 May) | 11 Days
14    2023-09-04   2023-09-11        LGW-MXP       MXP-LGW  ...       W45785               LGW         14    LGW-MXP (04 Sep) → MXP-LGW (11 Sep) | 7 Days
15    2023-10-04   2023-10-17        STN-MAD       MAD-STN  ...       FR5996               STN         15   STN-MAD (04 Oct) → MAD-STN (17 Oct) | 13 Days
16    2023-12-15   2024-01-10        STN-PEG       MAD-STN  ...       FR2497               STN         16   STN-PEG (15 Dec) → MAD-STN (10 Jan) | 26 Days
17    2024-04-24   2024-05-02        LHR-MAD       MAD-LHR  ...       BA0464               LHR         17    LHR-MAD (24 Apr) → MAD-LHR (02 May) | 8 Days
18    2024-06-26   2024-07-01        LHR-MAD       MAD-LGW  ...       IB3177               LHR         18    LHR-MAD (26 Jun) → MAD-LGW (01 Jul) | 5 Days

[19 rows x 9 columns]

  9 import pandas as pd
 10 import plotly.express as px
 11 from pathlib import Path
 12
 13 try:
 14     __file__
 15     TERMINAL = True
 16 except:
 17     TERMINAL = False
 18
 19 def display_flights_plotly_final(df):
 20     """
 21     Generates an interactive flight schedule plot ensuring each trip is on its
 22     own unique row. Includes custom text labels, month lines, and alternating
 23     background shading.
 24     """
 25     # --- 1. Data Pre-processing ---
 26     df = df.copy()
 27
 28     # Force columns to datetime, turning any errors into NaT (Not a Time)
 29     df['Outbound Date'] = pd.to_datetime(df['Outbound Date'], errors='coerce')
 30     df['Inbound Date'] = pd.to_datetime(df['Inbound Date'], errors='coerce')
 31
 32     # Strip the time component to prevent rendering glitches
 33     df['Outbound Date'] = df['Outbound Date'].dt.normalize()
 34     df['Inbound Date'] = df['Inbound Date'].dt.normalize()
 35
 36     # Compute extra information
 37     df['Days Difference'] = (df['Inbound Date'] - df['Outbound Date']).dt.days
 38     df['Departure Airport'] = df['Outbound Ports'].apply(lambda x: x.split('-')[0])
 39
 40     # Sort by date and reset the index. This index (0, 1, 2...) will be the
 41     # unique y-axis position for each trip, guaranteeing one row per trip.
 42     df = df.sort_values('Outbound Date', ascending=True).reset_index(drop=True)
 43
 44     # Hack: create y_axes_str to control order.
 45     # Calculate the required width (e.g., 2 for up to 99 items, 3 for up to 999)
 46     pad_width = len(str(len(df)))
 47     df['y_axis_str'] = df.index.astype(str).str.zfill(pad_width)
 48
 49     # Create the label for annotations and hovering
 50     df['Voyage Label'] = df.apply(
 51         lambda row: f"{row['Outbound Ports']} ({row['Outbound Date'].strftime('%d %b')}) → "
 52                     f"{row['Inbound Ports']} ({row['Inbound Date'].strftime('%d %b')}) | "
 53                     f"{row['Days Difference']} Days",
 54         axis=1
 55     )
 56
 57     # Show DataFrame to plot
 58     print("\nRoundtrips:")
 59     print(df)
 60
 61     # --- 2. Generate Background Shapes ---
 62     shapes = []
 63     min_date = df['Outbound Date'].min().normalize()
 64     max_date = df['Inbound Date'].max().normalize() + pd.DateOffset(months=1)
 65     month_starts = pd.date_range(start=min_date, end=max_date, freq='MS')
 66     for i, month_start in enumerate(month_starts):
 67         shapes.append({
 68             'type': 'line', 'xref': 'x', 'yref': 'paper',
 69             'x0': month_start, 'y0': 0, 'x1': month_start, 'y1': 1,
 70             'line': {'color': 'Gainsboro', 'width': 1, 'dash': 'dot'},
 71             'layer': 'below'
 72         })
 73         if i % 2 == 0:
 74             shapes.append({
 75                 'type': 'rect', 'xref': 'x', 'yref': 'paper',
 76                 'x0': month_start, 'y0': 0,
 77                 'x1': month_start + pd.DateOffset(months=1), 'y1': 1,
 78                 'fillcolor': 'LightGray', 'opacity': 0.1,
 79                 'line': {'width': 0},
 80                 'layer': 'below'
 81             })
 82
 83     # --- 3. Generate Annotations for Text Labels ---
 84     annotations = []
 85     for index, row in df.iterrows():
 86         annotations.append(
 87             dict(
 88                 x=row['Inbound Date'],
 89                 y=row['y_axis_str'], #index,  # Use the unique numerical index for the y-position
 90                 text=f"  {row['Voyage Label']}",
 91                 showarrow=False,
 92                 xanchor='left',
 93                 yanchor='middle',
 94                 align='left'
 95             )
 96         )
 97
 98     # --- 4. Core Plotting ---
 99     fig = px.timeline(
100         df,
101         x_start="Outbound Date",
102         x_end="Inbound Date",
103         y='y_axis_str', #df.index,  # KEY FIX: Use the unique index for the y-axis
104         color="Departure Airport",
105         hover_name="Voyage Label",
106         hover_data={'Days Difference': True},
107         title=f"Voyage Durations (Total Abroad: {df['Days Difference'].sum()} days)",
108         opacity=1.0,
109         category_orders={"y_axis_str": df.y_axis_str.tolist()[::-1]},
110         color_discrete_sequence=px.colors.qualitative.Pastel
111     )
112
113     # --- 5. Final Layout Updates ---
114     fig.update_layout(
115         plot_bgcolor='white',
116         xaxis_title="Date",
117         legend_title="Departure Airport",
118         shapes=shapes,
119         annotations=annotations,
120         legend=dict(
121             orientation="h",
122             yanchor="bottom",
123             y=1.02,
124             xanchor="right",
125             x=1
126         )
127     )
128
129     # Hide the meaningless y-axis numbers and title
130     fig.update_yaxes(
131         autorange="reversed",
132         showticklabels=False,
133         title_text=""
134     )
135
136     if not TERMINAL:
137         fig.update_yaxes(automargin=False)
138         fig.update_layout(
139             title=dict(
140                 x=0.5,  # Center the title
141                 y=0.95,  # Move it up slightly
142                 xanchor='center',
143                 yanchor='top'
144             ),
145             margin=dict(l=0, r=20, t=110, b=20)
146         )
147         # We could adjust x-axis to get use more space.
148
149     fig.show()
150
151
152
153     # Show
154     from plotly.io import show
155     show(fig)
156
157
158 # -------------------------------------------------------
159 # Main
160 # -------------------------------------------------------
161 # Libraries
162 from pathlib import Path
163
164 # Configuration
165 id = '1085721'
166 out_path = Path(f'./outputs/{id}')
167
168 try:
169     flight_df = pd.read_json(out_path / 'roundtrips.json')
170     flight_df['Outbound Date'] = pd.to_datetime(flight_df['Outbound Date'], unit='ms')
171     flight_df['Inbound Date'] = pd.to_datetime(flight_df['Inbound Date'], unit='ms')
172 except FileNotFoundError:
173     print(f"Error: File 'roundtrips.json' not found. Displaying sample data.")
174     sample_data = [
175         {"Outbound Date": "2024-01-15",
176          "Inbound Date": "2024-02-23",
177          "Outbound Ports": "LHR-JFK",
178          "Inbound Ports": "JFK-LHR",
179          "Voyage Code": "VS003"},
180         {"Outbound Date": "2024-03-05",
181          "Inbound Date": "2024-03-20",
182          "Outbound Ports": "LGW-BCN",
183          "Inbound Ports": "BCN-LGW",
184          "Voyage Code": "BA2712"},
185         {"Outbound Date": "2024-04-20",
186          "Inbound Date": "2024-05-15",
187          "Outbound Ports": "STN-FCO",
188          "Inbound Ports": "FCO-STN",
189          "Voyage Code": "FR123"},
190         {"Outbound Date": "2024-06-10",
191          "Inbound Date": "2024-06-25",
192          "Outbound Ports": "LHR-BOS",
193          "Inbound Ports": "BOS-LHR",
194          "Voyage Code": "VS011"}
195     ]
196     flight_df = pd.DataFrame(sample_data)
197     flight_df['Outbound Date'] = pd.to_datetime(flight_df['Outbound Date'])
198     flight_df['Inbound Date'] = pd.to_datetime(flight_df['Inbound Date'])
199
200 display_flights_plotly_final(flight_df)

Total running time of the script: ( 0 minutes 6.257 seconds)

Gallery generated by Sphinx-Gallery