eaglelandsonce commited on
Commit
06bfa2b
·
verified ·
1 Parent(s): 11cd52b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +453 -0
app.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import math
3
+ import base64
4
+ import datetime as dt
5
+ from dataclasses import dataclass
6
+ from typing import Optional, Tuple, List
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ import plotly.express as px
11
+ import plotly.graph_objects as go
12
+ import gradio as gr
13
+
14
+ # -----------------------------
15
+ # Helper: generate a fun demo dataset if user doesn't upload one
16
+ # -----------------------------
17
+
18
+ def make_demo_data(n_days: int = 180, seed: int = 7) -> pd.DataFrame:
19
+ rng = np.random.default_rng(seed)
20
+ start = dt.date.today() - dt.timedelta(days=n_days)
21
+ dates = pd.date_range(start, periods=n_days, freq="D")
22
+
23
+ categories = np.array(["Gadgets", "Gear", "Gifts"]) # product category
24
+ regions = np.array(["NA", "EMEA", "APAC"]) # sales region
25
+
26
+ cat = rng.choice(categories, size=n_days)
27
+ region = rng.choice(regions, size=n_days)
28
+
29
+ base = (
30
+ 200
31
+ + 20 * np.sin(np.linspace(0, 6 * math.pi, n_days)) # seasonality
32
+ + rng.normal(0, 15, n_days)
33
+ )
34
+
35
+ # Category and region effects
36
+ cat_bump = np.where(cat == "Gadgets", 25, np.where(cat == "Gear", 10, -5))
37
+ reg_bump = np.where(region == "NA", 15, np.where(region == "EMEA", 5, 0))
38
+
39
+ sales = np.clip(base + cat_bump + reg_bump, 10, None)
40
+ profit = np.clip(sales * (0.15 + rng.normal(0.0, 0.03, n_days)), 0, None)
41
+ customers = np.clip((sales / 10) + rng.normal(0, 2, n_days), 1, None)
42
+
43
+ df = pd.DataFrame(
44
+ {
45
+ "date": dates,
46
+ "category": cat,
47
+ "region": region,
48
+ "sales": sales.round(2),
49
+ "profit": profit.round(2),
50
+ "customers": customers.round(0).astype(int),
51
+ }
52
+ )
53
+ return df
54
+
55
+ # -----------------------------
56
+ # Core plotting logic
57
+ # -----------------------------
58
+
59
+ PLOT_TYPES = [
60
+ "line",
61
+ "scatter",
62
+ "bar",
63
+ "box",
64
+ "violin",
65
+ "heatmap (corr)",
66
+ ]
67
+
68
+ COLOR_SCALES = [
69
+ "Turbo",
70
+ "Viridis",
71
+ "Cividis",
72
+ "Plasma",
73
+ "Inferno",
74
+ "Magma",
75
+ "IceFire",
76
+ "Tealrose",
77
+ "Bluered",
78
+ ]
79
+
80
+ @dataclass
81
+ class PlotConfig:
82
+ plot_type: str
83
+ x: Optional[str]
84
+ y: Optional[str]
85
+ color: Optional[str]
86
+ facet_col: Optional[str]
87
+ agg: str
88
+ smooth: bool
89
+ smooth_window: int
90
+ add_trend: bool
91
+ trend_degree: int
92
+ points: bool
93
+ theme: str
94
+ color_scale: str
95
+
96
+
97
+ def _pick_numeric_cols(df: pd.DataFrame) -> List[str]:
98
+ return [c for c in df.columns if pd.api.types.is_numeric_dtype(df[c])]
99
+
100
+
101
+ def _pick_categorical_cols(df: pd.DataFrame) -> List[str]:
102
+ return [
103
+ c
104
+ for c in df.columns
105
+ if not pd.api.types.is_numeric_dtype(df[c]) and df[c].nunique() < len(df) / 2
106
+ ]
107
+
108
+
109
+ def _rolling_series(y: pd.Series, window: int) -> pd.Series:
110
+ window = max(2, int(window))
111
+ return y.rolling(window=window, min_periods=1, center=False).mean()
112
+
113
+
114
+ def _add_poly_trend(fig: go.Figure, xvals, yvals, degree: int, name: str = "Trend") -> None:
115
+ # Safeguard: only add if enough points and numeric
116
+ if len(xvals) < 3:
117
+ return
118
+ # Convert datetimes to ordinal for regression if needed
119
+ if np.issubdtype(np.array(xvals).dtype, np.datetime64):
120
+ x_numeric = pd.to_datetime(xvals).map(pd.Timestamp.toordinal).to_numpy()
121
+ else:
122
+ x_numeric = pd.Series(xvals).astype(float).to_numpy()
123
+
124
+ y_numeric = pd.Series(yvals).astype(float).to_numpy()
125
+ mask = np.isfinite(x_numeric) & np.isfinite(y_numeric)
126
+ x_numeric = x_numeric[mask]
127
+ y_numeric = y_numeric[mask]
128
+ if len(x_numeric) < max(5, degree + 2):
129
+ return
130
+
131
+ coeffs = np.polyfit(x_numeric, y_numeric, deg=degree)
132
+ xp = np.linspace(x_numeric.min(), x_numeric.max(), 200)
133
+ yp = np.polyval(coeffs, xp)
134
+
135
+ # Convert back datetime if original was datetime
136
+ if np.issubdtype(np.array(xvals).dtype, np.datetime64):
137
+ xp_plot = pd.to_datetime([dt.date.fromordinal(int(v)) for v in xp])
138
+ else:
139
+ xp_plot = xp
140
+
141
+ fig.add_trace(
142
+ go.Scatter(
143
+ x=xp_plot,
144
+ y=yp,
145
+ mode="lines",
146
+ name=name,
147
+ line=dict(color="#222", width=3, dash="dash"),
148
+ hovertemplate="%{x}<br>trend=%{y:.2f}<extra></extra>",
149
+ )
150
+ )
151
+
152
+
153
+ def build_plot(df: pd.DataFrame, cfg: PlotConfig) -> go.Figure:
154
+ # Basic validation
155
+ if df is None or df.empty:
156
+ return go.Figure()
157
+
158
+ # Auto-fill axes if missing
159
+ numeric_cols = _pick_numeric_cols(df)
160
+ cat_cols = _pick_categorical_cols(df)
161
+
162
+ x = cfg.x or ("date" if "date" in df.columns else (cat_cols[0] if cat_cols else (numeric_cols[0] if numeric_cols else None)))
163
+ y = cfg.y or (numeric_cols[0] if numeric_cols else None)
164
+
165
+ if cfg.plot_type == "heatmap (corr)":
166
+ num = df[numeric_cols].copy() if numeric_cols else pd.DataFrame()
167
+ if num.empty:
168
+ fig = go.Figure()
169
+ fig.add_annotation(text="No numeric columns for correlation heatmap.", showarrow=False)
170
+ return fig
171
+ corr = num.corr(numeric_only=True)
172
+ fig = px.imshow(
173
+ corr,
174
+ text_auto=True,
175
+ color_continuous_scale=cfg.color_scale,
176
+ aspect="auto",
177
+ title="Correlation Heatmap",
178
+ )
179
+ fig.update_layout(template=cfg.theme)
180
+ return fig
181
+
182
+ if x is None or y is None:
183
+ fig = go.Figure()
184
+ fig.add_annotation(text="Select X and Y columns to plot.", showarrow=False)
185
+ return fig
186
+
187
+ df_plot = df.copy()
188
+
189
+ # Optional aggregation for line/bar: group by X (and color if provided)
190
+ if cfg.agg != "none" and cfg.plot_type in {"line", "bar"}:
191
+ group_keys = [x] + ([cfg.color] if cfg.color else [])
192
+ if cfg.agg == "sum":
193
+ df_plot = df_plot.groupby(group_keys, dropna=False)[y].sum().reset_index()
194
+ elif cfg.agg == "mean":
195
+ df_plot = df_plot.groupby(group_keys, dropna=False)[y].mean().reset_index()
196
+ elif cfg.agg == "median":
197
+ df_plot = df_plot.groupby(group_keys, dropna=False)[y].median().reset_index()
198
+
199
+ # Build the base figure with Plotly Express
200
+ if cfg.plot_type == "line":
201
+ fig = px.line(
202
+ df_plot,
203
+ x=x,
204
+ y=y,
205
+ color=cfg.color,
206
+ facet_col=cfg.facet_col,
207
+ template=cfg.theme,
208
+ color_continuous_scale=cfg.color_scale,
209
+ )
210
+ elif cfg.plot_type == "scatter":
211
+ fig = px.scatter(
212
+ df_plot,
213
+ x=x,
214
+ y=y,
215
+ color=cfg.color,
216
+ facet_col=cfg.facet_col,
217
+ template=cfg.theme,
218
+ color_continuous_scale=cfg.color_scale,
219
+ render_mode="auto",
220
+ )
221
+ if cfg.points:
222
+ fig.update_traces(marker=dict(size=9, opacity=0.7))
223
+ elif cfg.plot_type == "bar":
224
+ fig = px.bar(
225
+ df_plot,
226
+ x=x,
227
+ y=y,
228
+ color=cfg.color,
229
+ facet_col=cfg.facet_col,
230
+ template=cfg.theme,
231
+ color_continuous_scale=cfg.color_scale,
232
+ )
233
+ elif cfg.plot_type == "box":
234
+ fig = px.box(
235
+ df_plot,
236
+ x=cfg.color if cfg.color else None,
237
+ y=y,
238
+ points="all" if cfg.points else False,
239
+ color=cfg.color,
240
+ template=cfg.theme,
241
+ color_discrete_sequence=px.colors.qualitative.Set2,
242
+ )
243
+ elif cfg.plot_type == "violin":
244
+ fig = px.violin(
245
+ df_plot,
246
+ x=cfg.color if cfg.color else None,
247
+ y=y,
248
+ box=True,
249
+ points="all" if cfg.points else False,
250
+ color=cfg.color,
251
+ template=cfg.theme,
252
+ color_discrete_sequence=px.colors.qualitative.Pastel,
253
+ )
254
+ else:
255
+ fig = go.Figure()
256
+
257
+ # Optional smoothing (rolling mean) for line/scatter
258
+ if cfg.smooth and cfg.plot_type in {"line", "scatter"}:
259
+ try:
260
+ if cfg.color and cfg.color in df_plot.columns:
261
+ for key, grp in df_plot.groupby(cfg.color):
262
+ sm = _rolling_series(grp[y].reset_index(drop=True), cfg.smooth_window)
263
+ fig.add_trace(
264
+ go.Scatter(
265
+ x=grp[x],
266
+ y=sm,
267
+ mode="lines",
268
+ name=f"{key} (rolling {cfg.smooth_window})",
269
+ line=dict(width=3),
270
+ opacity=0.85,
271
+ )
272
+ )
273
+ else:
274
+ sm = _rolling_series(df_plot[y], cfg.smooth_window)
275
+ fig.add_trace(
276
+ go.Scatter(
277
+ x=df_plot[x],
278
+ y=sm,
279
+ mode="lines",
280
+ name=f"rolling {cfg.smooth_window}",
281
+ line=dict(width=3),
282
+ opacity=0.85,
283
+ )
284
+ )
285
+ except Exception:
286
+ pass
287
+
288
+ # Optional polynomial trend for scatter/line (degree 1..3)
289
+ if cfg.add_trend and cfg.plot_type in {"line", "scatter"}:
290
+ if cfg.color and cfg.color in df_plot.columns:
291
+ for key, grp in df_plot.groupby(cfg.color):
292
+ _add_poly_trend(fig, grp[x], grp[y], cfg.trend_degree, name=f"{key} trend")
293
+ else:
294
+ _add_poly_trend(fig, df_plot[x], df_plot[y], cfg.trend_degree)
295
+
296
+ # Decorate
297
+ fig.update_layout(
298
+ title=f"{cfg.plot_type.title()} — {y} vs {x}",
299
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
300
+ hovermode="x unified" if cfg.plot_type == "line" else "closest",
301
+ margin=dict(l=40, r=20, t=60, b=40),
302
+ )
303
+
304
+ # Fun: subtle background stripes for readability
305
+ fig.update_xaxes(showgrid=True, gridcolor="rgba(0,0,0,0.06)")
306
+ fig.update_yaxes(showgrid=True, gridcolor="rgba(0,0,0,0.06)")
307
+
308
+ return fig
309
+
310
+ # -----------------------------
311
+ # Gradio UI
312
+ # -----------------------------
313
+
314
+ def infer_options(df: pd.DataFrame) -> Tuple[List[str], List[str]]:
315
+ num_cols = _pick_numeric_cols(df)
316
+ cat_cols = _pick_categorical_cols(df)
317
+ return num_cols, cat_cols
318
+
319
+
320
+ def read_csv(file: Optional[gr.File]) -> pd.DataFrame:
321
+ if file is None:
322
+ return make_demo_data()
323
+ try:
324
+ df = pd.read_csv(file.name)
325
+ except Exception:
326
+ # try excel
327
+ try:
328
+ df = pd.read_excel(file.name)
329
+ except Exception as e:
330
+ raise ValueError(f"Failed to read file: {e}")
331
+ return df
332
+
333
+
334
+ def ui_refresh_columns(file: Optional[gr.File]):
335
+ df = read_csv(file)
336
+ num_cols, cat_cols = infer_options(df)
337
+ all_cols = list(df.columns)
338
+ # reasonable defaults
339
+ x_default = "date" if "date" in all_cols else (cat_cols[0] if cat_cols else (num_cols[0] if num_cols else None))
340
+ y_default = num_cols[0] if num_cols else None
341
+
342
+ return (
343
+ gr.Dropdown.update(choices=all_cols, value=x_default),
344
+ gr.Dropdown.update(choices=num_cols, value=y_default),
345
+ gr.Dropdown.update(choices=all_cols + [None], value=None),
346
+ gr.Dropdown.update(choices=cat_cols + [None], value=None),
347
+ df.head(10),
348
+ )
349
+
350
+
351
+ def ui_plot(file, plot_type, x, y, color, facet_col, agg, smooth, smooth_window, add_trend, trend_degree, points, theme, color_scale):
352
+ df = read_csv(file)
353
+ cfg = PlotConfig(
354
+ plot_type=plot_type,
355
+ x=x,
356
+ y=y,
357
+ color=color if color != "None" else None,
358
+ facet_col=facet_col if facet_col != "None" else None,
359
+ agg=agg,
360
+ smooth=smooth,
361
+ smooth_window=int(smooth_window or 5),
362
+ add_trend=add_trend,
363
+ trend_degree=int(trend_degree or 1),
364
+ points=bool(points),
365
+ theme=theme,
366
+ color_scale=color_scale,
367
+ )
368
+ fig = build_plot(df, cfg)
369
+ return fig
370
+
371
+ with gr.Blocks(theme=gr.themes.Soft(primary="#2563eb", neutral="#111827")) as demo:
372
+ gr.Markdown(
373
+ """
374
+ # 📈 Data Viz Studio — with Gradio + Plotly
375
+ Upload a CSV (or use the built‑in demo), then craft an **interesting** plot.
376
+
377
+ **Tips**
378
+ - If your data has a `date` column, try a **Line** plot with a **rolling average** and a **trend**.
379
+ - Use **Color by** + **Facet** to spot differences across categories.
380
+ - Use **Heatmap (corr)** to quickly find strong relationships between numeric columns.
381
+ """
382
+ )
383
+
384
+ with gr.Row():
385
+ file = gr.File(label="Upload CSV or Excel (optional)")
386
+ refresh = gr.Button("🔄 Load / Refresh Columns", variant="secondary")
387
+ use_demo = gr.Button("🎲 Use Demo Data", variant="secondary")
388
+
389
+ with gr.Accordion("Data Preview", open=False):
390
+ preview = gr.Dataframe(row_count=(5, "dynamic"), wrap=True)
391
+
392
+ with gr.Row():
393
+ plot_type = gr.Dropdown(PLOT_TYPES, value="line", label="Plot Type")
394
+ theme = gr.Dropdown(px.colors.named_themes(), value="plotly_white", label="Theme")
395
+ color_scale = gr.Dropdown(COLOR_SCALES, value="Turbo", label="Color Scale (for continuous)")
396
+
397
+ with gr.Row():
398
+ x = gr.Dropdown(choices=[], label="X Axis")
399
+ y = gr.Dropdown(choices=[], label="Y Axis (numeric)")
400
+
401
+ with gr.Row():
402
+ color = gr.Dropdown(choices=[], value=None, label="Color by (categorical or numeric)")
403
+ facet_col = gr.Dropdown(choices=[], value=None, label="Facet column (categorical)")
404
+ agg = gr.Dropdown(["none", "sum", "mean", "median"], value="none", label="Aggregate (line/bar)")
405
+
406
+ with gr.Row():
407
+ smooth = gr.Checkbox(value=True, label="Add Rolling Average")
408
+ smooth_window = gr.Slider(2, 60, value=7, step=1, label="Rolling Window (periods)")
409
+ add_trend = gr.Checkbox(value=False, label="Add Polynomial Trendline")
410
+ trend_degree = gr.Slider(1, 3, value=1, step=1, label="Trend Degree (1-3)")
411
+ points = gr.Checkbox(value=True, label="Show Points (scatter/box/violin)")
412
+
413
+ out = gr.Plot(label="Your Plot")
414
+
415
+ # Wire events
416
+ def set_demo(_):
417
+ df = make_demo_data()
418
+ # Save to an in-memory CSV for preview (not strictly necessary for plotting)
419
+ num_cols, cat_cols = infer_options(df)
420
+ all_cols = list(df.columns)
421
+ x_default = "date" if "date" in all_cols else (cat_cols[0] if cat_cols else (num_cols[0] if num_cols else None))
422
+ y_default = num_cols[0] if num_cols else None
423
+ return (
424
+ None,
425
+ gr.Dropdown.update(choices=all_cols, value=x_default),
426
+ gr.Dropdown.update(choices=num_cols, value=y_default),
427
+ gr.Dropdown.update(choices=all_cols + [None], value=None),
428
+ gr.Dropdown.update(choices=cat_cols + [None], value=None),
429
+ df.head(10),
430
+ )
431
+
432
+ refresh.click(ui_refresh_columns, inputs=[file], outputs=[x, y, color, facet_col, preview])
433
+ use_demo.click(set_demo, inputs=[use_demo], outputs=[file, x, y, color, facet_col, preview])
434
+
435
+ # Auto-refresh columns on file change
436
+ file.change(ui_refresh_columns, inputs=[file], outputs=[x, y, color, facet_col, preview])
437
+
438
+ # Draw plot when any control changes
439
+ controls = [file, plot_type, x, y, color, facet_col, agg, smooth, smooth_window, add_trend, trend_degree, points, theme, color_scale]
440
+ for c in controls:
441
+ c.change(ui_plot, inputs=controls, outputs=out)
442
+
443
+ # Initial state using demo data
444
+ _ = set_demo(None)
445
+ out.update(value=build_plot(make_demo_data(), PlotConfig(
446
+ plot_type="line", x="date", y="sales", color="region", facet_col=None,
447
+ agg="mean", smooth=True, smooth_window=7, add_trend=False, trend_degree=1,
448
+ points=False, theme="plotly_white", color_scale="Turbo"
449
+ )))
450
+
451
+ if __name__ == "__main__":
452
+ demo.launch() # You can set share=True when running locally to create a public link
453
+