Spaces:
Running
Running
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()
|