Note
Click here to download the full example code
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)