File size: 8,320 Bytes
74ead6a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1fa456e
 
 
 
 
74ead6a
1fa456e
 
 
74ead6a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import gradio as gr
import pandas as pd

from nomad_data import country_emoji_map, data

# Create dataframe from imported data
df = pd.DataFrame(data)

# Create styling functions
def style_quality_of_life(val):
    """Style the Quality of Life column with color gradient from red to green"""
    if pd.isna(val):
        # Special styling for null/missing values
        return 'background-color: rgba(200, 200, 200, 0.2); color: #999; font-style: italic;'
    
    # Define min and max values for Quality of Life (typically on a scale of 0-10)
    min_val = 5.0  # Anything below this will be bright red
    max_val = 9.0  # Anything above this will be bright green
    
    # Normalize value between 0 and 1
    normalized = (val - min_val) / (max_val - min_val)
    # Clamp between 0 and 1
    normalized = max(0, min(normalized, 1))
    
    # Calculate percentage fill for gradient
    percentage = int(normalized * 100)
    
    # Create a linear gradient based on the normalized value
    if normalized < 0.5:
        # Red to yellow gradient
        start_color = f"rgba(255, {int(255 * (normalized * 2))}, 0, 0.3)"
        end_color = "rgba(255, 255, 255, 0)"
    else:
        # Yellow to green gradient
        start_color = f"rgba({int(255 * (1 - (normalized - 0.5) * 2))}, 255, 0, 0.3)"
        end_color = "rgba(255, 255, 255, 0)"
    
    return f'background: linear-gradient(to right, {start_color} {percentage}%, {end_color} {percentage}%)'

def style_internet_speed(val):
    """Style the Internet Speed column from red (slow) to green (fast)"""
    if pd.isna(val):
        # Special styling for null/missing values
        return 'background-color: rgba(200, 200, 200, 0.2); color: #999; font-style: italic;'
    
    # Define min and max values
    min_val = 20   # Slow internet
    max_val = 300  # Fast internet
    
    # Normalize value between 0 and 1
    normalized = (val - min_val) / (max_val - min_val)
    # Clamp between 0 and 1
    normalized = max(0, min(normalized, 1))
    
    # Calculate percentage fill for gradient
    percentage = int(normalized * 100)
    
    # Create a linear gradient based on the normalized value
    if normalized < 0.5:
        # Red to yellow gradient
        start_color = f"rgba(255, {int(255 * (normalized * 2))}, 0, 0.3)"
        end_color = "rgba(255, 255, 255, 0)"
    else:
        # Yellow to green gradient
        start_color = f"rgba({int(255 * (1 - (normalized - 0.5) * 2))}, 255, 0, 0.3)"
        end_color = "rgba(255, 255, 255, 0)"
    
    return f'background: linear-gradient(to right, {start_color} {percentage}%, {end_color} {percentage}%)'

def style_dataframe(df):
    """Apply styling to the entire dataframe"""
    # Create a copy to avoid SettingWithCopyWarning
    styled_df = df.copy()
    
    # Convert to Styler object
    styler = styled_df.style
    
    # Apply styles to specific columns
    styler = styler.applymap(style_quality_of_life, subset=['Quality of Life'])
    styler = styler.applymap(style_internet_speed, subset=['Internet Speed (Mbps)'])
    
    # Highlight null values in all columns
    styler = styler.highlight_null(props='color: #999; font-style: italic; background-color: rgba(200, 200, 200, 0.2)')
    
    # Format numeric columns
    styler = styler.format({
        'Quality of Life': lambda x: f'{x:.1f}' if pd.notna(x) else 'Data Not Available',
        'Internet Speed (Mbps)': lambda x: f'{x:.1f}' if pd.notna(x) else 'Data Not Available',
        'Monthly Cost Living (USD)': lambda x: f'${x:.0f}' if pd.notna(x) else 'Data Not Available',
        'Visa Length (Months)': lambda x: f'{x:.0f}' if pd.notna(x) else 'Data Not Available',
        'Visa Cost (USD)': lambda x: f'${x:.0f}' if pd.notna(x) else 'Data Not Available',
        'Growth Trend (5 Years)': lambda x: f'{x}' if pd.notna(x) else 'Data Not Available'
    })
    
    return styler

def filter_data(country, max_cost):
    """Filter data based on country and maximum cost of living"""
    filtered_df = df.copy()
    
    if country and country != "All":
        filtered_df = filtered_df[filtered_df["Country"] == country]
    
    # Filter by maximum cost of living (and handle null values)
    if max_cost < df["Monthly Cost Living (USD)"].max():
        # Include rows where cost is less than max_cost OR cost is null
        cost_mask = (filtered_df["Monthly Cost Living (USD)"] <= max_cost) | (filtered_df["Monthly Cost Living (USD)"].isna())
        filtered_df = filtered_df[cost_mask]
    
    return style_dataframe(filtered_df)

# Function to get unique values for dropdowns with "All" option
def get_unique_values(column):
    unique_values = ["All"] + sorted(df[column].unique().tolist())
    return unique_values

# Add country emojis for the dropdown
def get_country_with_emoji(column):
    choices_with_emoji = ["โœˆ๏ธ All"]
    for c in df[column].unique():
        if c in country_emoji_map:
            choices_with_emoji.append(country_emoji_map[c])
        else:
            choices_with_emoji.append(c)
    return sorted(choices_with_emoji)

# Initial styled dataframe
styled_df = style_dataframe(df)

with gr.Blocks(css="""
    .gradio-container .table-wrap {
        font-family: 'Inter', sans-serif;
    }
    .gradio-container table td, .gradio-container table th {
        text-align: left;
    }
    .gradio-container table th {
        background-color: #f3f4f6;
        font-weight: 600;
    }
    /* Style for null values */
    .null-value {
        color: #999;
        font-style: italic;
        background-color: rgba(200, 200, 200, 0.2);
    }
    .title {
        font-size: 3rem;
        font-weight: 600;
        text-align: center;
    }
""") as demo:
    gr.HTML(elem_classes="title", value="๐ŸŒ")
    gr.HTML("<a href='https://www.fontspace.com/category/graffiti'><img src='https://see.fontimg.com/api/rf5/JpZqa/MWMyNzc2ODk3OTFlNDk2OWJkY2VjYTIzNzFlY2E4MWIudHRm/bm9tYWQgZGVzdGluYXRpb25z/super-feel.png?r=fs&h=130&w=2000&fg=e2e2e2&bg=FFFFFF&tb=1&s=65' alt='Graffiti fonts'></a>")

    gr.Markdown("Explore top digital nomad locations around the world. The bars in numeric columns indicate relative values - longer bars are better!")
    
    with gr.Row():
        country_dropdown = gr.Dropdown(
            choices=get_country_with_emoji("Country"),
            value="โœˆ๏ธ All",
            label="๐ŸŒ Filter by Country"
        )
        
        cost_slider = gr.Slider(
            minimum=500,
            maximum=4000,
            value=4000,
            step=100,
            label="๐Ÿ’ฐ Maximum Monthly Cost of Living (USD)"
        )
    
    
    data_table = gr.Dataframe(
        value=styled_df,
        datatype=["str", "str", "number", "number", "number", "str", "number", "number", "str", "str"],
        max_height=600,
        interactive=False,
        show_copy_button=True,
        show_row_numbers=True,
        show_search=True,
        show_fullscreen_button=True,
        pinned_columns=2
    )
    
    # Update data when filters change
    def process_country_filter(country, cost):
        # Remove emoji from country name if present
        if country and country.startswith("โœˆ๏ธ All"):
            country = "All"
        else:
            for emoji_code in ["๐Ÿ‡ง๐Ÿ‡ท", "๐Ÿ‡ญ๐Ÿ‡บ", "๐Ÿ‡บ๐Ÿ‡พ", "๐Ÿ‡ต๐Ÿ‡น", "๐Ÿ‡ฌ๐Ÿ‡ช", "๐Ÿ‡น๐Ÿ‡ญ", "๐Ÿ‡ฆ๐Ÿ‡ช", "๐Ÿ‡ช๐Ÿ‡ธ", "๐Ÿ‡ฎ๐Ÿ‡น", "๐Ÿ‡จ๐Ÿ‡ฆ", "๐Ÿ‡จ๐Ÿ‡ด", "๐Ÿ‡ฒ๐Ÿ‡ฝ", "๐Ÿ‡ฏ๐Ÿ‡ต", "๐Ÿ‡ฐ๐Ÿ‡ท"]:
                if country and emoji_code in country:
                    country = country.split(" ", 1)[1]
                    break
                    
        filtered_df = df.copy()
        
        # Filter by country
        if country and country != "All":
            filtered_df = filtered_df[filtered_df["Country"] == country]
        
        # Filter by cost with special handling for nulls
        if cost < df["Monthly Cost Living (USD)"].max():
            cost_mask = (filtered_df["Monthly Cost Living (USD)"] <= cost) & (filtered_df["Monthly Cost Living (USD)"].notna())
        
            filtered_df = filtered_df[cost_mask]
        
        return style_dataframe(filtered_df)
    
    country_dropdown.change(process_country_filter, [country_dropdown, cost_slider], data_table)
    cost_slider.change(process_country_filter, [country_dropdown, cost_slider], data_table)
    
demo.launch()