File size: 17,783 Bytes
67b2513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c1aed8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67b2513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c1aed8
 
 
67b2513
 
 
 
2c1aed8
 
67b2513
 
 
 
 
 
 
 
 
 
 
 
2c1aed8
 
 
 
 
 
 
 
 
 
 
 
 
67b2513
 
 
 
 
 
2c1aed8
67b2513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c1aed8
 
 
 
 
 
 
 
 
 
 
 
 
 
67b2513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c1aed8
67b2513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c1aed8
67b2513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
import json
import warnings
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
import pycountry
import gradio as gr

# Suppress pydantic field alias warning from Gradio
warnings.filterwarnings("ignore", message=".*alias.*session_hash.*")
warnings.filterwarnings("ignore", message=".*UnsupportedFieldAttributeWarning.*")

# Load data
with open('dl_stats_0102_0109_top10k.json', 'r') as f:
    dl_by_date = json.load(f)

# Helper function: Convert ISO-2 to ISO-3
def iso2_to_iso3(code):
    """Convert 2-letter ISO code to 3-letter ISO code"""
    if pd.isna(code) or code == 'null':
        return None
    try:
        return pycountry.countries.get(alpha_2=code).alpha_3
    except (AttributeError, KeyError):
        return None

# Choropleth function
def plot_downloads_choropleth(repo_id=None, user_id=None, norm_type='region-log', date=None):
    """Create a choropleth map showing downloads by country."""
    if date not in dl_by_date:
        raise ValueError(f"Date '{date}' not found")
    
    repo_downloads = dl_by_date[date]['repo_downloads']
    org_downloads = dl_by_date[date]['org_downloads']
    total_downloads = dl_by_date[date]['total_downloads']
    
    # Get the appropriate downloads data
    if repo_id and repo_id != "None":
        if repo_id not in repo_downloads:
            raise ValueError(f"Repo '{repo_id}' not found")
        downloads_data = repo_downloads[repo_id]['downloads_by_region']
        title_prefix = f"Repo: {repo_id}"
    elif user_id and user_id != "None":
        if user_id not in org_downloads:
            raise ValueError(f"User/Org '{user_id}' not found")
        downloads_data = org_downloads[user_id]['downloads_by_region']
        title_prefix = f"User/Org: {user_id}"
    else:
        downloads_data = total_downloads['downloads_by_region']
        title_prefix = "Global"
    
    # Prepare data for choropleth
    use_log = norm_type.endswith('-log')
    is_global_norm = norm_type.startswith('global')
    
    # Calculate ranks by country if showing specific org/repo
    ranks_by_country = {}
    if repo_id and repo_id != "None":
        # Calculate repo ranks by country
        for code_2 in downloads_data.keys():
            if code_2 not in total_downloads['downloads_by_region']:
                continue
            repos_in_country = sorted([
                (name, repo_dct['downloads_by_region'].get(code_2, 0))
                for name, repo_dct in repo_downloads.items()
            ], key=lambda x: x[1], reverse=True)
            # Find rank (1-indexed)
            for rank, (name, _) in enumerate(repos_in_country, 1):
                if name == repo_id:
                    ranks_by_country[code_2] = rank
                    break
    elif user_id and user_id != "None":
        # Calculate org ranks by country
        for code_2 in downloads_data.keys():
            if code_2 not in total_downloads['downloads_by_region']:
                continue
            orgs_in_country = sorted([
                (name, org_dct['downloads_by_region'].get(code_2, 0))
                for name, org_dct in org_downloads.items()
            ], key=lambda x: x[1], reverse=True)
            # Find rank (1-indexed)
            for rank, (name, _) in enumerate(orgs_in_country, 1):
                if name == user_id:
                    ranks_by_country[code_2] = rank
                    break
    
    choropleth_data = []
    for code_2, downloads in downloads_data.items():
        code_3 = iso2_to_iso3(code_2)
        if code_3 is None:
            continue
        
        # Calculate normalized value
        if is_global_norm:
            total = total_downloads['total_downloads']
        else:
            total = total_downloads['downloads_by_region'].get(code_2, 0)
        
        if total <= 0:
            continue
        
        value = downloads / total
        color_value = np.log10(value + 1e-10) if use_log else value
        
        # Get rank if available
        rank = ranks_by_country.get(code_2, None)
        
        choropleth_data.append({
            'country_code': code_3,
            'value': color_value,
            'proportion': value,
            'downloads': downloads,
            'rank': rank
        })
    
    if not choropleth_data:
        raise ValueError("No valid data to plot")
    
    df_map = pd.DataFrame(choropleth_data)
    
    # Create color label
    color_label = 'Proportion of Global Downloads' if is_global_norm else "Proportion of Country's Downloads"
    if use_log:
        color_label += ' (log10)'
    
    # Prepare hover data
    hover_data_dict = {
        'downloads': ':,.0f',
        'proportion': ':.4%',
        'value': False
    }
    # Add rank if available (only for specific org/repo)
    has_rank = 'rank' in df_map.columns and df_map['rank'].notna().any()
    if has_rank:
        # Format rank as string with # prefix, handling None values
        df_map['rank_display'] = df_map['rank'].apply(lambda x: f"#{int(x)}" if pd.notna(x) else "N/A")
        hover_data_dict['rank_display'] = True
    
    # Create choropleth map
    fig = px.choropleth(
        df_map,
        locations='country_code',
        color='value',
        hover_name='country_code',
        hover_data=hover_data_dict,
        color_continuous_scale='Viridis',
        title=f'{title_prefix} - {color_label} ({date})',
        labels={'value': color_label, 'country_code': 'Country'}
    )
    
    fig.update_layout(
        geo=dict(showframe=False, showcoastlines=True),
        height=600
    )
    
    return fig

# Top downloads function
def plot_top_downloads(date, data_type='orgs', country_code_iso3=None, top_n=30, use_log=True, show_percentage=True):
    """Create a plot showing top orgs or repos for a given date."""
    if date not in dl_by_date:
        raise ValueError(f"Date '{date}' not found")
    
    date_data = dl_by_date[date]
    downloads_dict = date_data['org_downloads'] if data_type == 'orgs' else date_data['repo_downloads']
    total_dl_dict = date_data['total_downloads']
    
    # Convert ISO-3 to ISO-2 if country code provided
    if country_code_iso3 and country_code_iso3 != "None":
        try:
            country = pycountry.countries.get(alpha_3=country_code_iso3)
            country_code_iso2 = country.alpha_2
            country_name = country.name
        except (AttributeError, KeyError):
            raise ValueError(f"Invalid ISO-3 country code: {country_code_iso3}")
    else:
        country_code_iso2 = None
        country_name = "Global"
    
    # Get total downloads for percentage calculation
    if country_code_iso2 is None:
        total_dl = total_dl_dict['total_downloads']
    else:
        total_dl = total_dl_dict['downloads_by_region'].get(country_code_iso2, 0)
    
    # Get top items
    if country_code_iso2 is None:
        data = sorted([
            (name, dl_dct['total_downloads'])
            for name, dl_dct in downloads_dict.items()
        ], key=lambda x: x[1], reverse=True)[:top_n]
    else:
        data = sorted([
            (name, dl_dct['downloads_by_region'].get(country_code_iso2, 0))
            for name, dl_dct in downloads_dict.items()
        ], key=lambda x: x[1], reverse=True)
        data = [(name, dl) for name, dl in data if dl > 0][:top_n]
    
    if not data:
        raise ValueError(f"No data found for {data_type} in {country_name}")
    
    # Prepare data for plotting
    names = [name for name, _ in data]
    downloads = [dl for _, dl in data]
    percentages = [dl / total_dl * 100 if total_dl > 0 else 0 for dl in downloads]
    
    # Prepare text labels
    if show_percentage:
        text_labels = [f'{dl:,.0f}<br>({pct:.2f}%)' for dl, pct in zip(downloads, percentages)]
    else:
        text_labels = [f'{dl:,.0f}' for dl in downloads]
    
    # Determine x-axis values
    x_values = [np.log10(dl + 1) if use_log else dl for dl in downloads]
    
    # Calculate x-axis range
    if x_values:
        x_min = min(x_values)
        x_max = max(x_values)
        x_range = x_max - x_min
        padding = x_range * 0.1
        x_range_min = max(0, x_min - padding) if not use_log else x_min - padding
        x_range_max = x_max + padding
    else:
        x_range_min = 0
        x_range_max = 1
    
    # Create figure
    fig = go.Figure()
    
    fig.add_trace(go.Bar(
        y=names,
        x=x_values,
        orientation='h',
        text=text_labels,
        textposition='outside',
        textfont=dict(size=11),
        marker_color='lightblue' if data_type == 'orgs' else 'lightcoral',
        hovertemplate='<b>%{y}</b><br>Downloads: %{customdata:,.0f}<extra></extra>',
        customdata=downloads
    ))
    
    # Update layout
    xaxis_title = 'Downloads (log10)' if use_log else 'Downloads'
    title_type = 'Users/Orgs' if data_type == 'orgs' else 'Repos'
    
    fig.update_layout(
        title_text=f'Top {len(data)} {title_type} - {country_name} ({date})',
        height=max(600, len(data) * 40 + 100),
        xaxis_title=xaxis_title,
        font=dict(size=14)
    )
    
    fig.update_yaxes(
        autorange="reversed",
        tickangle=-45,
        tickfont=dict(size=14)
    )
    
    fig.update_xaxes(range=[x_range_min, x_range_max])
    
    return fig

# Prepare dropdown options
# Separate week aggregations from daily dates
all_dates = list(dl_by_date.keys())
week_dates = [d for d in all_dates if 'week' in d]
daily_dates = [d for d in all_dates if 'week' not in d]

# Sort each group
week_dates = sorted(week_dates)
daily_dates = sorted(daily_dates)

# Combine: week aggregations first, then daily dates
dates = week_dates + daily_dates

# Set default date (prefer week aggregation if available)
default_date = "Jan-2026-week-02-to-09" if "Jan-2026-week-02-to-09" in dates else (dates[-1] if dates else None)

# Country dropdown options
country_options = ["None"]
for country in pycountry.countries:
    iso2 = country.alpha_2
    iso3 = country.alpha_3
    name = country.name
    country_options.append(f"{iso2} / {iso3} - {name}")

# Gradio interface functions
def create_map(map_type, org_textbox, repo_textbox, date, norm_type):
    """Create choropleth map."""
    repo_id = None if map_type != "by repo" else (None if not repo_textbox or repo_textbox.strip() == "" else repo_textbox.strip())
    user_id = None if map_type != "by org" else (None if not org_textbox or org_textbox.strip() == "" else org_textbox.strip())
    
    if date is None:
        return None
    
    try:
        fig = plot_downloads_choropleth(
            repo_id=repo_id,
            user_id=user_id,
            norm_type=norm_type,
            date=date
        )
        return fig
    except Exception as e:
        return f"Error: {str(e)}"

def create_top_downloads(region, repo_type, date, top_n, use_log, show_percentage):
    """Create top downloads plot."""
    if date is None:
        return None
    
    # Parse region
    country_code_iso3 = None
    if region and region != "None":
        # Extract ISO-3 code from "ISO2 / ISO3 - Name" format
        parts = region.split(" / ")
        if len(parts) >= 2:
            country_code_iso3 = parts[1].split(" - ")[0]
    
    try:
        fig = plot_top_downloads(
            date=date,
            data_type=repo_type,
            country_code_iso3=country_code_iso3,
            top_n=top_n,
            use_log=use_log,
            show_percentage=show_percentage
        )
        return fig
    except Exception as e:
        return f"Error: {str(e)}"

# Create Gradio interface
with gr.Blocks(title="Hugging Face Downloads Analysis") as app:
    gr.Markdown("""
    # Hugging Face Repository Downloads Analysis
    
    This dashboard visualizes download statistics for Hugging Face repositories, datasets, and models.
    Explore download patterns by organization, repository, country, and date.
    """)
    
    with gr.Row():
        # Main content area
        with gr.Column(scale=4):
            # Map section
            with gr.Accordion("Choropleth Map - Downloads by Country", open=True):
                with gr.Row():
                    map_type = gr.Radio(
                        choices=["everything", "by org", "by repo"],
                        value="everything",
                        label="Map Type"
                    )
                    map_date = gr.Dropdown(
                        choices=dates,
                        value=default_date,
                        label="Date"
                    )
                
                with gr.Row():
                    map_org = gr.Textbox(
                        value="",
                        label="Organization (if 'by org' selected)",
                        placeholder="e.g., Qwen, sentence-transformers",
                        visible=False,
                        info="Enter organization/user name and press Enter (e.g., 'Qwen' or 'sentence-transformers')"
                    )
                    map_repo = gr.Textbox(
                        value="",
                        label="Repository (if 'by repo' selected)",
                        placeholder="e.g., sentence-transformers/all-MiniLM-L6-v2",
                        visible=False,
                        info="Enter full repo name and press Enter (e.g., 'sentence-transformers/all-MiniLM-L6-v2')"
                    )
                
                map_output = gr.Plot(label="Choropleth Map")
            
            # Top downloads section
            with gr.Accordion("Top Downloads - By Organization or Repository", open=True):
                with gr.Row():
                    top_region = gr.Dropdown(
                        choices=country_options,
                        value="None",
                        label="Region (ISO-2 / ISO-3 - Country Name)"
                    )
                    top_repo_type = gr.Radio(
                        choices=["orgs", "repos"],
                        value="orgs",
                        label="Show"
                    )
                    top_date = gr.Dropdown(
                        choices=dates,
                        value=default_date,
                        label="Date"
                    )
                
                top_output = gr.Plot(label="Top Downloads")
        
        # Sidebar with config options
        with gr.Column(scale=1):
            with gr.Group():
                gr.Markdown("### Map Configuration")
                map_norm_type = gr.Radio(
                    choices=["global", "global-log", "region", "region-log"],
                    value="global-log",
                    label="Normalization Type"
                )
            
            with gr.Group():
                gr.Markdown("### Top Downloads Configuration")
                top_n = gr.Slider(
                    minimum=5,
                    maximum=100,
                    value=30,
                    step=5,
                    label="Number of Items"
                )
                top_use_log = gr.Checkbox(
                    value=True,
                    label="Use Log Scale"
                )
                top_show_pct = gr.Checkbox(
                    value=True,
                    label="Show Percentage"
                )
    
    # Helper function to update textbox visibility
    def update_map_textboxes(map_type):
        """Update visibility of org and repo textboxes."""
        return (
            gr.Textbox(visible=(map_type == "by org")),
            gr.Textbox(visible=(map_type == "by repo"))
        )
    
    # Update map org/repo textboxes visibility based on map type
    map_type.change(
        fn=update_map_textboxes,
        inputs=[map_type],
        outputs=[map_org, map_repo]
    )
    
    # Create map
    map_type.change(fn=create_map, inputs=[map_type, map_org, map_repo, map_date, map_norm_type], outputs=[map_output])
    map_org.submit(fn=create_map, inputs=[map_type, map_org, map_repo, map_date, map_norm_type], outputs=[map_output])
    map_repo.submit(fn=create_map, inputs=[map_type, map_org, map_repo, map_date, map_norm_type], outputs=[map_output])
    map_date.change(fn=create_map, inputs=[map_type, map_org, map_repo, map_date, map_norm_type], outputs=[map_output])
    map_norm_type.change(fn=create_map, inputs=[map_type, map_org, map_repo, map_date, map_norm_type], outputs=[map_output])
    
    # Create top downloads
    top_region.change(fn=create_top_downloads, inputs=[top_region, top_repo_type, top_date, top_n, top_use_log, top_show_pct], outputs=[top_output])
    top_repo_type.change(fn=create_top_downloads, inputs=[top_region, top_repo_type, top_date, top_n, top_use_log, top_show_pct], outputs=[top_output])
    top_date.change(fn=create_top_downloads, inputs=[top_region, top_repo_type, top_date, top_n, top_use_log, top_show_pct], outputs=[top_output])
    top_n.change(fn=create_top_downloads, inputs=[top_region, top_repo_type, top_date, top_n, top_use_log, top_show_pct], outputs=[top_output])
    top_use_log.change(fn=create_top_downloads, inputs=[top_region, top_repo_type, top_date, top_n, top_use_log, top_show_pct], outputs=[top_output])
    top_show_pct.change(fn=create_top_downloads, inputs=[top_region, top_repo_type, top_date, top_n, top_use_log, top_show_pct], outputs=[top_output])
    
    # Generate initial plots
    def init_app():
        if default_date:
            map_fig = create_map("everything", "", "", default_date, "region-log")
            top_fig = create_top_downloads("None", "orgs", default_date, 30, True, True)
            return map_fig, top_fig
        return None, None
    
    app.load(fn=init_app, outputs=[map_output, top_output])

if __name__ == "__main__":
    app.launch()