Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import pandas as pd
|
3 |
+
|
4 |
+
from nomad_data import country_emoji_map, data
|
5 |
+
|
6 |
+
# Create dataframe from imported data
|
7 |
+
df = pd.DataFrame(data)
|
8 |
+
|
9 |
+
# Create styling functions
|
10 |
+
def style_quality_of_life(val):
|
11 |
+
"""Style the Quality of Life column with color gradient from red to green"""
|
12 |
+
if pd.isna(val):
|
13 |
+
# Special styling for null/missing values
|
14 |
+
return 'background-color: rgba(200, 200, 200, 0.2); color: #999; font-style: italic;'
|
15 |
+
|
16 |
+
# Define min and max values for Quality of Life (typically on a scale of 0-10)
|
17 |
+
min_val = 5.0 # Anything below this will be bright red
|
18 |
+
max_val = 9.0 # Anything above this will be bright green
|
19 |
+
|
20 |
+
# Normalize value between 0 and 1
|
21 |
+
normalized = (val - min_val) / (max_val - min_val)
|
22 |
+
# Clamp between 0 and 1
|
23 |
+
normalized = max(0, min(normalized, 1))
|
24 |
+
|
25 |
+
# Calculate percentage fill for gradient
|
26 |
+
percentage = int(normalized * 100)
|
27 |
+
|
28 |
+
# Create a linear gradient based on the normalized value
|
29 |
+
if normalized < 0.5:
|
30 |
+
# Red to yellow gradient
|
31 |
+
start_color = f"rgba(255, {int(255 * (normalized * 2))}, 0, 0.3)"
|
32 |
+
end_color = "rgba(255, 255, 255, 0)"
|
33 |
+
else:
|
34 |
+
# Yellow to green gradient
|
35 |
+
start_color = f"rgba({int(255 * (1 - (normalized - 0.5) * 2))}, 255, 0, 0.3)"
|
36 |
+
end_color = "rgba(255, 255, 255, 0)"
|
37 |
+
|
38 |
+
return f'background: linear-gradient(to right, {start_color} {percentage}%, {end_color} {percentage}%)'
|
39 |
+
|
40 |
+
def style_internet_speed(val):
|
41 |
+
"""Style the Internet Speed column from red (slow) to green (fast)"""
|
42 |
+
if pd.isna(val):
|
43 |
+
# Special styling for null/missing values
|
44 |
+
return 'background-color: rgba(200, 200, 200, 0.2); color: #999; font-style: italic;'
|
45 |
+
|
46 |
+
# Define min and max values
|
47 |
+
min_val = 20 # Slow internet
|
48 |
+
max_val = 300 # Fast internet
|
49 |
+
|
50 |
+
# Normalize value between 0 and 1
|
51 |
+
normalized = (val - min_val) / (max_val - min_val)
|
52 |
+
# Clamp between 0 and 1
|
53 |
+
normalized = max(0, min(normalized, 1))
|
54 |
+
|
55 |
+
# Calculate percentage fill for gradient
|
56 |
+
percentage = int(normalized * 100)
|
57 |
+
|
58 |
+
# Create a linear gradient based on the normalized value
|
59 |
+
if normalized < 0.5:
|
60 |
+
# Red to yellow gradient
|
61 |
+
start_color = f"rgba(255, {int(255 * (normalized * 2))}, 0, 0.3)"
|
62 |
+
end_color = "rgba(255, 255, 255, 0)"
|
63 |
+
else:
|
64 |
+
# Yellow to green gradient
|
65 |
+
start_color = f"rgba({int(255 * (1 - (normalized - 0.5) * 2))}, 255, 0, 0.3)"
|
66 |
+
end_color = "rgba(255, 255, 255, 0)"
|
67 |
+
|
68 |
+
return f'background: linear-gradient(to right, {start_color} {percentage}%, {end_color} {percentage}%)'
|
69 |
+
|
70 |
+
def style_dataframe(df):
|
71 |
+
"""Apply styling to the entire dataframe"""
|
72 |
+
# Create a copy to avoid SettingWithCopyWarning
|
73 |
+
styled_df = df.copy()
|
74 |
+
|
75 |
+
# Convert to Styler object
|
76 |
+
styler = styled_df.style
|
77 |
+
|
78 |
+
# Apply styles to specific columns
|
79 |
+
styler = styler.applymap(style_quality_of_life, subset=['Quality of Life'])
|
80 |
+
styler = styler.applymap(style_internet_speed, subset=['Internet Speed (Mbps)'])
|
81 |
+
|
82 |
+
# Highlight null values in all columns
|
83 |
+
styler = styler.highlight_null(props='color: #999; font-style: italic; background-color: rgba(200, 200, 200, 0.2)')
|
84 |
+
|
85 |
+
# Format numeric columns
|
86 |
+
styler = styler.format({
|
87 |
+
'Quality of Life': lambda x: f'{x:.1f}' if pd.notna(x) else 'Data Not Available',
|
88 |
+
'Internet Speed (Mbps)': lambda x: f'{x:.1f}' if pd.notna(x) else 'Data Not Available',
|
89 |
+
'Monthly Cost Living (USD)': lambda x: f'${x:.0f}' if pd.notna(x) else 'Data Not Available',
|
90 |
+
'Visa Length (Months)': lambda x: f'{x:.0f}' if pd.notna(x) else 'Data Not Available',
|
91 |
+
'Visa Cost (USD)': lambda x: f'${x:.0f}' if pd.notna(x) else 'Data Not Available',
|
92 |
+
'Growth Trend (5 Years)': lambda x: f'{x}' if pd.notna(x) else 'Data Not Available'
|
93 |
+
})
|
94 |
+
|
95 |
+
return styler
|
96 |
+
|
97 |
+
def filter_data(country, max_cost):
|
98 |
+
"""Filter data based on country and maximum cost of living"""
|
99 |
+
filtered_df = df.copy()
|
100 |
+
|
101 |
+
if country and country != "All":
|
102 |
+
filtered_df = filtered_df[filtered_df["Country"] == country]
|
103 |
+
|
104 |
+
# Filter by maximum cost of living (and handle null values)
|
105 |
+
if max_cost < df["Monthly Cost Living (USD)"].max():
|
106 |
+
# Include rows where cost is less than max_cost OR cost is null
|
107 |
+
cost_mask = (filtered_df["Monthly Cost Living (USD)"] <= max_cost) | (filtered_df["Monthly Cost Living (USD)"].isna())
|
108 |
+
filtered_df = filtered_df[cost_mask]
|
109 |
+
|
110 |
+
return style_dataframe(filtered_df)
|
111 |
+
|
112 |
+
# Function to get unique values for dropdowns with "All" option
|
113 |
+
def get_unique_values(column):
|
114 |
+
unique_values = ["All"] + sorted(df[column].unique().tolist())
|
115 |
+
return unique_values
|
116 |
+
|
117 |
+
# Add country emojis for the dropdown
|
118 |
+
def get_country_with_emoji(column):
|
119 |
+
choices_with_emoji = ["โ๏ธ All"]
|
120 |
+
for c in df[column].unique():
|
121 |
+
if c in country_emoji_map:
|
122 |
+
choices_with_emoji.append(country_emoji_map[c])
|
123 |
+
else:
|
124 |
+
choices_with_emoji.append(c)
|
125 |
+
return sorted(choices_with_emoji)
|
126 |
+
|
127 |
+
# Initial styled dataframe
|
128 |
+
styled_df = style_dataframe(df)
|
129 |
+
|
130 |
+
with gr.Blocks(css="""
|
131 |
+
.gradio-container .table-wrap {
|
132 |
+
font-family: 'Inter', sans-serif;
|
133 |
+
}
|
134 |
+
.gradio-container table td, .gradio-container table th {
|
135 |
+
text-align: left;
|
136 |
+
}
|
137 |
+
.gradio-container table th {
|
138 |
+
background-color: #f3f4f6;
|
139 |
+
font-weight: 600;
|
140 |
+
}
|
141 |
+
/* Style for null values */
|
142 |
+
.null-value {
|
143 |
+
color: #999;
|
144 |
+
font-style: italic;
|
145 |
+
background-color: rgba(200, 200, 200, 0.2);
|
146 |
+
}
|
147 |
+
""") as demo:
|
148 |
+
gr.Markdown("# ๐ Digital Nomad Destinations")
|
149 |
+
gr.Markdown("Explore top digital nomad locations around the world. The bars in numeric columns indicate relative values - longer bars are better!")
|
150 |
+
|
151 |
+
with gr.Row():
|
152 |
+
country_dropdown = gr.Dropdown(
|
153 |
+
choices=get_country_with_emoji("Country"),
|
154 |
+
value="โ๏ธ All",
|
155 |
+
label="๐ Filter by Country"
|
156 |
+
)
|
157 |
+
|
158 |
+
cost_slider = gr.Slider(
|
159 |
+
minimum=500,
|
160 |
+
maximum=4000,
|
161 |
+
value=4000,
|
162 |
+
step=100,
|
163 |
+
label="๐ฐ Maximum Monthly Cost of Living (USD)"
|
164 |
+
)
|
165 |
+
|
166 |
+
|
167 |
+
data_table = gr.Dataframe(
|
168 |
+
value=styled_df,
|
169 |
+
datatype=["str", "str", "number", "number", "number", "str", "number", "number", "str", "str"],
|
170 |
+
max_height=600,
|
171 |
+
interactive=False,
|
172 |
+
show_copy_button=True,
|
173 |
+
show_row_numbers=True,
|
174 |
+
show_search=True,
|
175 |
+
show_fullscreen_button=True,
|
176 |
+
pinned_columns=2
|
177 |
+
)
|
178 |
+
|
179 |
+
# Update data when filters change
|
180 |
+
def process_country_filter(country, cost):
|
181 |
+
# Remove emoji from country name if present
|
182 |
+
if country and country.startswith("โ๏ธ All"):
|
183 |
+
country = "All"
|
184 |
+
else:
|
185 |
+
for emoji_code in ["๐ง๐ท", "๐ญ๐บ", "๐บ๐พ", "๐ต๐น", "๐ฌ๐ช", "๐น๐ญ", "๐ฆ๐ช", "๐ช๐ธ", "๐ฎ๐น", "๐จ๐ฆ", "๐จ๐ด", "๐ฒ๐ฝ", "๐ฏ๐ต", "๐ฐ๐ท"]:
|
186 |
+
if country and emoji_code in country:
|
187 |
+
country = country.split(" ", 1)[1]
|
188 |
+
break
|
189 |
+
|
190 |
+
filtered_df = df.copy()
|
191 |
+
|
192 |
+
# Filter by country
|
193 |
+
if country and country != "All":
|
194 |
+
filtered_df = filtered_df[filtered_df["Country"] == country]
|
195 |
+
|
196 |
+
# Filter by cost with special handling for nulls
|
197 |
+
if cost < df["Monthly Cost Living (USD)"].max():
|
198 |
+
cost_mask = (filtered_df["Monthly Cost Living (USD)"] <= cost) & (filtered_df["Monthly Cost Living (USD)"].notna())
|
199 |
+
|
200 |
+
filtered_df = filtered_df[cost_mask]
|
201 |
+
|
202 |
+
return style_dataframe(filtered_df)
|
203 |
+
|
204 |
+
country_dropdown.change(process_country_filter, [country_dropdown, cost_slider], data_table)
|
205 |
+
cost_slider.change(process_country_filter, [country_dropdown, cost_slider], data_table)
|
206 |
+
|
207 |
+
gr.Markdown("### ๐ Data Visualization Guide")
|
208 |
+
gr.Markdown("The table above uses colorful gradient bars to help you quickly identify: \n"
|
209 |
+
"- **๐ Quality of Life**: Longer green bars indicate higher quality of life \n"
|
210 |
+
"- **๐ Internet Speed**: Longer green bars indicate faster internet connections \n"
|
211 |
+
"- **๐ต Cost of Living**: Values shown as dollar amounts without color coding \n"
|
212 |
+
"- **โ Missing Data**: Displayed as *Data Not Available* with a light gray background")
|
213 |
+
|
214 |
+
gr.Markdown("### ๐งณ Digital Nomad Tips")
|
215 |
+
gr.Markdown("- Look for places with digital nomad visas for longer stays \n"
|
216 |
+
"- Consider internet speed if you need to attend video meetings \n"
|
217 |
+
"- Balance cost of living with quality of life for the best experience \n"
|
218 |
+
"- Some newer nomad destinations may have incomplete data")
|
219 |
+
|
220 |
+
demo.launch()
|