File size: 8,408 Bytes
c2a7494
 
fdce7f7
c2a7494
7b72661
 
c2a7494
7b72661
c2a7494
 
5f4b467
 
 
 
 
 
 
 
c2a7494
7b72661
 
 
 
 
 
b9b50e9
 
 
 
7b72661
b9b50e9
 
 
 
 
 
 
 
 
 
 
 
 
7b72661
 
 
b9b50e9
 
 
7b72661
 
b9b50e9
7b72661
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9b50e9
7b72661
b9b50e9
c2a7494
fdce7f7
2c8188d
7b72661
fdce7f7
 
 
 
b9b50e9
7b72661
 
 
fdce7f7
7b72661
fdce7f7
 
 
 
 
c10b3f9
fdce7f7
7b72661
c10b3f9
fdce7f7
 
7b72661
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1326591
c10b3f9
7b72661
 
 
 
 
 
 
c10b3f9
c2a7494
7b72661
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5f4b467
c10b3f9
5f4b467
7b72661
 
c10b3f9
 
7b72661
 
 
 
5f4b467
c2a7494
7b72661
b9b50e9
7b72661
 
 
 
 
 
c2a7494
7b72661
b9b50e9
7b72661
 
 
 
 
5f4b467
7b72661
c10b3f9
b9b50e9
7b72661
 
c10b3f9
 
 
 
7b72661
c10b3f9
b9b50e9
5f4b467
7b72661
 
 
5f4b467
7b72661
b9b50e9
7b72661
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9b50e9
 
7b72661
c10b3f9
b9b50e9
7b72661
b9b50e9
 
 
7b72661
b9b50e9
 
7b72661
b9b50e9
5f4b467
c2a7494
b9b50e9
c10b3f9
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
import pandas as pd
import gradio as gr
import plotly.express as px

# --- 1. Helper Functions ---

def categorize_rank(r):
    """Categorizes the rank into SEO buckets."""
    if pd.isna(r) or r == 0 or r == "NA":
        return "Not Talking (Intent Not Correct)"
    elif r <= 3: return "01–03"
    elif r <= 10: return "04–10"
    elif r <= 20: return "11–20"
    elif r <= 30: return "21–30"
    elif r <= 40: return "31–40"
    elif r <= 50: return "41–50"
    elif r <= 100: return "51–100"
    else: return "100+"

def clean_and_process_sheet(df):
    """
    Takes a raw dataframe from a single sheet, cleans columns, 
    identifies dates, and adds Rank Range columns.
    """
    # Clean Headers
    df.columns = df.columns.map(lambda x: str(x).strip())
    non_date_cols = ["Keyword", "Avg. monthly searches"]
    available_non_date = [c for c in non_date_cols if c in df.columns]
    
    # Identify Date Columns
    potential_date_cols = [col for col in df.columns if col not in non_date_cols and "Unnamed" not in col]
    rename_dict = {}
    for col in potential_date_cols:
        try:
            cleaned = str(col).replace("_", " ").replace(".", " ").strip()
            d = pd.to_datetime(cleaned, errors="coerce", dayfirst=True)
            if pd.notna(d):
                rename_dict[col] = d.strftime("%d_%b_%Y")
        except: pass
    
    df.rename(columns=rename_dict, inplace=True)
    date_cols = list(rename_dict.values())
    
    # Build Processed DataFrame
    processed_df = df[available_non_date].copy()
    
    for col in date_cols:
        range_col_name = f"{col}_Rank Range"
        raw_vals = pd.to_numeric(df[col], errors="coerce")
        processed_df[f"{col}_Rank_Numeric"] = raw_vals.fillna(0).astype(int)
        processed_df[range_col_name] = raw_vals.apply(categorize_rank)
        
    return processed_df

def build_chart_and_filters(df):
    """
    Generates the Plotly figure and filter choices for a specific dataframe.
    """
    if df is None or df.empty:
        return None, [], []

    # 1. Prepare Data for Chart
    viz_data = []
    # Identify all date columns by looking for "_Rank Range"
    range_cols = [c for c in df.columns if "_Rank Range" in c]
    
    for range_col in range_cols:
        # Extract date from column name (e.g., "27_Oct_2025_Rank Range" -> "27_Oct_2025")
        month_name = range_col.replace("_Rank Range", "")
        
        counts = df[range_col].value_counts().reset_index()
        counts.columns = ['Range', 'Keyword Count']
        counts['Month'] = month_name
        viz_data.append(counts)

    full_viz_df = pd.concat(viz_data) if viz_data else pd.DataFrame()
    
    # 2. Define Sorting Order
    correct_order = [
        "01–03", "04–10", "11–20", "21–30", "31–40", 
        "41–50", "51–100", "100+", "Not Talking (Intent Not Correct)"
    ]
    
    fig = None
    unique_months = []
    
    if not full_viz_df.empty:
        unique_months = list(full_viz_df['Month'].unique())
        fig = px.bar(
            full_viz_df, 
            x='Range', 
            y='Keyword Count', 
            color='Month',
            barmode='group', # Side-by-side bars
            text_auto=True,
            title="Keyword Performance Distribution",
            category_orders={"Range": correct_order}
        )
    
    return fig, unique_months, correct_order


# --- 2. Gradio Event Functions ---

def process_upload_initial(file):
    """
    Reads ALL sheets, stores them in State, and renders the FIRST sheet.
    """
    if file is None:
        return None, None, gr.Dropdown(choices=[]), None, None, gr.Dropdown(choices=[]), gr.Dropdown(choices=[])
    
    excel = pd.ExcelFile(file.name)
    all_sheets_data = {}
    
    # Process every sheet and store in dictionary
    for sheet_name in excel.sheet_names:
        raw_df = pd.read_excel(file.name, sheet_name=sheet_name)
        all_sheets_data[sheet_name] = clean_and_process_sheet(raw_df)
    
    sheet_names = list(all_sheets_data.keys())
    first_sheet_name = sheet_names[0]
    first_df = all_sheets_data[first_sheet_name]
    
    # Generate View for First Sheet
    fig, months, ranges = build_chart_and_filters(first_df)
    
    return (
        all_sheets_data,                                      # State: All Data
        first_df,                                             # State: Current Sheet Data
        gr.Dropdown(choices=sheet_names, value=first_sheet_name), # Sheet Selector
        fig,                                                  # Chart
        first_df,                                             # Table
        gr.Dropdown(choices=months, value=months[0] if months else None), # Month Filter
        gr.Dropdown(choices=ranges, value=ranges[0] if ranges else None)  # Range Filter
    )

def change_sheet(selected_sheet, all_data):
    """
    Switches the view when a new sheet is selected from dropdown.
    """
    if not selected_sheet or not all_data:
        return None, None, None, gr.Dropdown(choices=[]), gr.Dropdown(choices=[])
        
    new_df = all_data[selected_sheet]
    fig, months, ranges = build_chart_and_filters(new_df)
    
    return (
        new_df, # Update Current Sheet State
        fig,    # Update Chart
        new_df, # Update Table
        gr.Dropdown(choices=months, value=months[0] if months else None), # Update Month Choices
        gr.Dropdown(choices=ranges, value=ranges[0] if ranges else None)  # Update Range Choices
    )

def filter_table(current_df, selected_month, selected_range):
    """
    Filters the CURRENTLY active sheet's dataframe.
    """
    if current_df is None or current_df.empty: return None
    
    target_col = f"{selected_month}_Rank Range"
    
    if target_col in current_df.columns:
        filtered_df = current_df[current_df[target_col] == selected_range]
        return filtered_df
    
    return current_df

def reset_table(current_df):
    return current_df


# --- 3. UI Layout ---

with gr.Blocks(theme=gr.themes.Soft()) as demo:
    # STATES
    all_sheets_state = gr.State({}) # Stores DICTIONARY of all sheets {name: df}
    current_sheet_state = gr.State(pd.DataFrame()) # Stores currently visible sheet DF

    gr.Markdown("# 🚀 SEO Multi-Sheet Dashboard")
    
    # TOP ROW: Upload & Sheet Selection
    with gr.Row():
        file_input = gr.File(label="Upload Excel (Multiple Sheets Supported)", file_types=[".xlsx"])
        with gr.Column():
            process_btn = gr.Button("📊 Load File", variant="primary")
            # NEW: Sheet Selector Dropdown
            sheet_dropdown = gr.Dropdown(label="📑 Select Sheet to Analyze", choices=[], interactive=True)

    # VISUALIZATION
    plot_output = gr.Plot(label="Keyword Distribution")
    
    # FILTER ROW
    gr.Markdown("### 🔍 Filter Data")
    with gr.Row():
        month_dropdown = gr.Dropdown(label="Select Month", choices=[])
        range_dropdown = gr.Dropdown(label="Select Rank Range", choices=[])
        filter_btn = gr.Button("Apply Filter")
        reset_btn = gr.Button("Show All Rows")

    table_output = gr.DataFrame(interactive=False)

    # --- EVENTS ---
    
    # 1. Upload & Process (Loads all sheets, defaults to first)
    process_btn.click(
        fn=process_upload_initial,
        inputs=file_input,
        outputs=[
            all_sheets_state,    # Save all sheets
            current_sheet_state, # Save current sheet
            sheet_dropdown,      # Update dropdown options
            plot_output,         # Show chart
            table_output,        # Show table
            month_dropdown,      # Update filters
            range_dropdown
        ]
    )
    
    # 2. Change Sheet (User selects "RD Monthly" etc.)
    sheet_dropdown.change(
        fn=change_sheet,
        inputs=[sheet_dropdown, all_sheets_state],
        outputs=[
            current_sheet_state,
            plot_output,
            table_output,
            month_dropdown,
            range_dropdown
        ]
    )

    # 3. Filter Data (Applied to current sheet)
    filter_btn.click(
        fn=filter_table,
        inputs=[current_sheet_state, month_dropdown, range_dropdown],
        outputs=table_output
    )

    # 4. Reset Data
    reset_btn.click(
        fn=reset_table,
        inputs=current_sheet_state,
        outputs=table_output
    )

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