Spaces:
Runtime error
Runtime error
| import io | |
| import math | |
| import base64 | |
| import datetime as dt | |
| from dataclasses import dataclass | |
| from typing import Optional, Tuple, List | |
| import numpy as np | |
| import pandas as pd | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| import gradio as gr | |
| # ----------------------------- | |
| # Helper: generate a fun demo dataset if user doesn't upload one | |
| # ----------------------------- | |
| def make_demo_data(n_days: int = 180, seed: int = 7) -> pd.DataFrame: | |
| rng = np.random.default_rng(seed) | |
| start = dt.date.today() - dt.timedelta(days=n_days) | |
| dates = pd.date_range(start, periods=n_days, freq="D") | |
| categories = np.array(["Gadgets", "Gear", "Gifts"]) # product category | |
| regions = np.array(["NA", "EMEA", "APAC"]) # sales region | |
| cat = rng.choice(categories, size=n_days) | |
| region = rng.choice(regions, size=n_days) | |
| base = ( | |
| 200 | |
| + 20 * np.sin(np.linspace(0, 6 * math.pi, n_days)) # seasonality | |
| + rng.normal(0, 15, n_days) | |
| ) | |
| # Category and region effects | |
| cat_bump = np.where(cat == "Gadgets", 25, np.where(cat == "Gear", 10, -5)) | |
| reg_bump = np.where(region == "NA", 15, np.where(region == "EMEA", 5, 0)) | |
| sales = np.clip(base + cat_bump + reg_bump, 10, None) | |
| profit = np.clip(sales * (0.15 + rng.normal(0.0, 0.03, n_days)), 0, None) | |
| customers = np.clip((sales / 10) + rng.normal(0, 2, n_days), 1, None) | |
| df = pd.DataFrame( | |
| { | |
| "date": dates, | |
| "category": cat, | |
| "region": region, | |
| "sales": sales.round(2), | |
| "profit": profit.round(2), | |
| "customers": customers.round(0).astype(int), | |
| } | |
| ) | |
| return df | |
| # ----------------------------- | |
| # Core plotting logic | |
| # ----------------------------- | |
| PLOT_TYPES = [ | |
| "line", | |
| "scatter", | |
| "bar", | |
| "box", | |
| "violin", | |
| "heatmap (corr)", | |
| ] | |
| COLOR_SCALES = [ | |
| "Turbo", | |
| "Viridis", | |
| "Cividis", | |
| "Plasma", | |
| "Inferno", | |
| "Magma", | |
| "IceFire", | |
| "Tealrose", | |
| "Bluered", | |
| ] | |
| class PlotConfig: | |
| plot_type: str | |
| x: Optional[str] | |
| y: Optional[str] | |
| color: Optional[str] | |
| facet_col: Optional[str] | |
| agg: str | |
| smooth: bool | |
| smooth_window: int | |
| add_trend: bool | |
| trend_degree: int | |
| points: bool | |
| theme: str | |
| color_scale: str | |
| def _pick_numeric_cols(df: pd.DataFrame) -> List[str]: | |
| return [c for c in df.columns if pd.api.types.is_numeric_dtype(df[c])] | |
| def _pick_categorical_cols(df: pd.DataFrame) -> List[str]: | |
| return [ | |
| c | |
| for c in df.columns | |
| if not pd.api.types.is_numeric_dtype(df[c]) and df[c].nunique() < len(df) / 2 | |
| ] | |
| def _rolling_series(y: pd.Series, window: int) -> pd.Series: | |
| window = max(2, int(window)) | |
| return y.rolling(window=window, min_periods=1, center=False).mean() | |
| def _add_poly_trend(fig: go.Figure, xvals, yvals, degree: int, name: str = "Trend") -> None: | |
| # Safeguard: only add if enough points and numeric | |
| if len(xvals) < 3: | |
| return | |
| # Convert datetimes to ordinal for regression if needed | |
| if np.issubdtype(np.array(xvals).dtype, np.datetime64): | |
| x_numeric = pd.to_datetime(xvals).map(pd.Timestamp.toordinal).to_numpy() | |
| else: | |
| x_numeric = pd.Series(xvals).astype(float).to_numpy() | |
| y_numeric = pd.Series(yvals).astype(float).to_numpy() | |
| mask = np.isfinite(x_numeric) & np.isfinite(y_numeric) | |
| x_numeric = x_numeric[mask] | |
| y_numeric = y_numeric[mask] | |
| if len(x_numeric) < max(5, degree + 2): | |
| return | |
| coeffs = np.polyfit(x_numeric, y_numeric, deg=degree) | |
| xp = np.linspace(x_numeric.min(), x_numeric.max(), 200) | |
| yp = np.polyval(coeffs, xp) | |
| # Convert back datetime if original was datetime | |
| if np.issubdtype(np.array(xvals).dtype, np.datetime64): | |
| xp_plot = pd.to_datetime([dt.date.fromordinal(int(v)) for v in xp]) | |
| else: | |
| xp_plot = xp | |
| fig.add_trace( | |
| go.Scatter( | |
| x=xp_plot, | |
| y=yp, | |
| mode="lines", | |
| name=name, | |
| line=dict(color="#222", width=3, dash="dash"), | |
| hovertemplate="%{x}<br>trend=%{y:.2f}<extra></extra>", | |
| ) | |
| ) | |
| def build_plot(df: pd.DataFrame, cfg: PlotConfig) -> go.Figure: | |
| # Basic validation | |
| if df is None or df.empty: | |
| return go.Figure() | |
| # Auto-fill axes if missing | |
| numeric_cols = _pick_numeric_cols(df) | |
| cat_cols = _pick_categorical_cols(df) | |
| 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))) | |
| y = cfg.y or (numeric_cols[0] if numeric_cols else None) | |
| if cfg.plot_type == "heatmap (corr)": | |
| num = df[numeric_cols].copy() if numeric_cols else pd.DataFrame() | |
| if num.empty: | |
| fig = go.Figure() | |
| fig.add_annotation(text="No numeric columns for correlation heatmap.", showarrow=False) | |
| return fig | |
| corr = num.corr(numeric_only=True) | |
| fig = px.imshow( | |
| corr, | |
| text_auto=True, | |
| color_continuous_scale=cfg.color_scale, | |
| aspect="auto", | |
| title="Correlation Heatmap", | |
| ) | |
| fig.update_layout(template=cfg.theme) | |
| return fig | |
| if x is None or y is None: | |
| fig = go.Figure() | |
| fig.add_annotation(text="Select X and Y columns to plot.", showarrow=False) | |
| return fig | |
| df_plot = df.copy() | |
| # Optional aggregation for line/bar: group by X (and color if provided) | |
| if cfg.agg != "none" and cfg.plot_type in {"line", "bar"}: | |
| group_keys = [x] + ([cfg.color] if cfg.color else []) | |
| if cfg.agg == "sum": | |
| df_plot = df_plot.groupby(group_keys, dropna=False)[y].sum().reset_index() | |
| elif cfg.agg == "mean": | |
| df_plot = df_plot.groupby(group_keys, dropna=False)[y].mean().reset_index() | |
| elif cfg.agg == "median": | |
| df_plot = df_plot.groupby(group_keys, dropna=False)[y].median().reset_index() | |
| # Build the base figure with Plotly Express | |
| if cfg.plot_type == "line": | |
| fig = px.line( | |
| df_plot, | |
| x=x, | |
| y=y, | |
| color=cfg.color, | |
| facet_col=cfg.facet_col, | |
| template=cfg.theme, | |
| color_continuous_scale=cfg.color_scale, | |
| ) | |
| elif cfg.plot_type == "scatter": | |
| fig = px.scatter( | |
| df_plot, | |
| x=x, | |
| y=y, | |
| color=cfg.color, | |
| facet_col=cfg.facet_col, | |
| template=cfg.theme, | |
| color_continuous_scale=cfg.color_scale, | |
| render_mode="auto", | |
| ) | |
| if cfg.points: | |
| fig.update_traces(marker=dict(size=9, opacity=0.7)) | |
| elif cfg.plot_type == "bar": | |
| fig = px.bar( | |
| df_plot, | |
| x=x, | |
| y=y, | |
| color=cfg.color, | |
| facet_col=cfg.facet_col, | |
| template=cfg.theme, | |
| color_continuous_scale=cfg.color_scale, | |
| ) | |
| elif cfg.plot_type == "box": | |
| fig = px.box( | |
| df_plot, | |
| x=cfg.color if cfg.color else None, | |
| y=y, | |
| points="all" if cfg.points else False, | |
| color=cfg.color, | |
| template=cfg.theme, | |
| color_discrete_sequence=px.colors.qualitative.Set2, | |
| ) | |
| elif cfg.plot_type == "violin": | |
| fig = px.violin( | |
| df_plot, | |
| x=cfg.color if cfg.color else None, | |
| y=y, | |
| box=True, | |
| points="all" if cfg.points else False, | |
| color=cfg.color, | |
| template=cfg.theme, | |
| color_discrete_sequence=px.colors.qualitative.Pastel, | |
| ) | |
| else: | |
| fig = go.Figure() | |
| # Optional smoothing (rolling mean) for line/scatter | |
| if cfg.smooth and cfg.plot_type in {"line", "scatter"}: | |
| try: | |
| if cfg.color and cfg.color in df_plot.columns: | |
| for key, grp in df_plot.groupby(cfg.color): | |
| sm = _rolling_series(grp[y].reset_index(drop=True), cfg.smooth_window) | |
| fig.add_trace( | |
| go.Scatter( | |
| x=grp[x], | |
| y=sm, | |
| mode="lines", | |
| name=f"{key} (rolling {cfg.smooth_window})", | |
| line=dict(width=3), | |
| opacity=0.85, | |
| ) | |
| ) | |
| else: | |
| sm = _rolling_series(df_plot[y], cfg.smooth_window) | |
| fig.add_trace( | |
| go.Scatter( | |
| x=df_plot[x], | |
| y=sm, | |
| mode="lines", | |
| name=f"rolling {cfg.smooth_window}", | |
| line=dict(width=3), | |
| opacity=0.85, | |
| ) | |
| ) | |
| except Exception: | |
| pass | |
| # Optional polynomial trend for scatter/line (degree 1..3) | |
| if cfg.add_trend and cfg.plot_type in {"line", "scatter"}: | |
| if cfg.color and cfg.color in df_plot.columns: | |
| for key, grp in df_plot.groupby(cfg.color): | |
| _add_poly_trend(fig, grp[x], grp[y], cfg.trend_degree, name=f"{key} trend") | |
| else: | |
| _add_poly_trend(fig, df_plot[x], df_plot[y], cfg.trend_degree) | |
| # Decorate | |
| fig.update_layout( | |
| title=f"{cfg.plot_type.title()} — {y} vs {x}", | |
| legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1), | |
| hovermode="x unified" if cfg.plot_type == "line" else "closest", | |
| margin=dict(l=40, r=20, t=60, b=40), | |
| ) | |
| # Fun: subtle background stripes for readability | |
| fig.update_xaxes(showgrid=True, gridcolor="rgba(0,0,0,0.06)") | |
| fig.update_yaxes(showgrid=True, gridcolor="rgba(0,0,0,0.06)") | |
| return fig | |
| # ----------------------------- | |
| # Gradio UI | |
| # ----------------------------- | |
| def infer_options(df: pd.DataFrame) -> Tuple[List[str], List[str]]: | |
| num_cols = _pick_numeric_cols(df) | |
| cat_cols = _pick_categorical_cols(df) | |
| return num_cols, cat_cols | |
| def read_csv(file: Optional[gr.File]) -> pd.DataFrame: | |
| if file is None: | |
| return make_demo_data() | |
| try: | |
| df = pd.read_csv(file.name) | |
| except Exception: | |
| # try excel | |
| try: | |
| df = pd.read_excel(file.name) | |
| except Exception as e: | |
| raise ValueError(f"Failed to read file: {e}") | |
| return df | |
| def ui_refresh_columns(file: Optional[gr.File]): | |
| df = read_csv(file) | |
| num_cols, cat_cols = infer_options(df) | |
| all_cols = list(df.columns) | |
| # reasonable defaults | |
| x_default = "date" if "date" in all_cols else (cat_cols[0] if cat_cols else (num_cols[0] if num_cols else None)) | |
| y_default = num_cols[0] if num_cols else None | |
| return ( | |
| gr.Dropdown.update(choices=all_cols, value=x_default), | |
| gr.Dropdown.update(choices=num_cols, value=y_default), | |
| gr.Dropdown.update(choices=all_cols + [None], value=None), | |
| gr.Dropdown.update(choices=cat_cols + [None], value=None), | |
| df.head(10), | |
| ) | |
| def ui_plot(file, plot_type, x, y, color, facet_col, agg, smooth, smooth_window, add_trend, trend_degree, points, theme, color_scale): | |
| df = read_csv(file) | |
| cfg = PlotConfig( | |
| plot_type=plot_type, | |
| x=x, | |
| y=y, | |
| color=color if color != "None" else None, | |
| facet_col=facet_col if facet_col != "None" else None, | |
| agg=agg, | |
| smooth=smooth, | |
| smooth_window=int(smooth_window or 5), | |
| add_trend=add_trend, | |
| trend_degree=int(trend_degree or 1), | |
| points=bool(points), | |
| theme=theme, | |
| color_scale=color_scale, | |
| ) | |
| fig = build_plot(df, cfg) | |
| return fig | |
| with gr.Blocks(theme=gr.themes.Soft(primary="#2563eb", neutral="#111827")) as demo: | |
| gr.Markdown( | |
| """ | |
| # 📈 Data Viz Studio — with Gradio + Plotly | |
| Upload a CSV (or use the built‑in demo), then craft an **interesting** plot. | |
| **Tips** | |
| - If your data has a `date` column, try a **Line** plot with a **rolling average** and a **trend**. | |
| - Use **Color by** + **Facet** to spot differences across categories. | |
| - Use **Heatmap (corr)** to quickly find strong relationships between numeric columns. | |
| """ | |
| ) | |
| with gr.Row(): | |
| file = gr.File(label="Upload CSV or Excel (optional)") | |
| refresh = gr.Button("🔄 Load / Refresh Columns", variant="secondary") | |
| use_demo = gr.Button("🎲 Use Demo Data", variant="secondary") | |
| with gr.Accordion("Data Preview", open=False): | |
| preview = gr.Dataframe(row_count=(5, "dynamic"), wrap=True) | |
| with gr.Row(): | |
| plot_type = gr.Dropdown(PLOT_TYPES, value="line", label="Plot Type") | |
| theme = gr.Dropdown(px.colors.named_themes(), value="plotly_white", label="Theme") | |
| color_scale = gr.Dropdown(COLOR_SCALES, value="Turbo", label="Color Scale (for continuous)") | |
| with gr.Row(): | |
| x = gr.Dropdown(choices=[], label="X Axis") | |
| y = gr.Dropdown(choices=[], label="Y Axis (numeric)") | |
| with gr.Row(): | |
| color = gr.Dropdown(choices=[], value=None, label="Color by (categorical or numeric)") | |
| facet_col = gr.Dropdown(choices=[], value=None, label="Facet column (categorical)") | |
| agg = gr.Dropdown(["none", "sum", "mean", "median"], value="none", label="Aggregate (line/bar)") | |
| with gr.Row(): | |
| smooth = gr.Checkbox(value=True, label="Add Rolling Average") | |
| smooth_window = gr.Slider(2, 60, value=7, step=1, label="Rolling Window (periods)") | |
| add_trend = gr.Checkbox(value=False, label="Add Polynomial Trendline") | |
| trend_degree = gr.Slider(1, 3, value=1, step=1, label="Trend Degree (1-3)") | |
| points = gr.Checkbox(value=True, label="Show Points (scatter/box/violin)") | |
| out = gr.Plot(label="Your Plot") | |
| # Wire events | |
| def set_demo(_): | |
| df = make_demo_data() | |
| # Save to an in-memory CSV for preview (not strictly necessary for plotting) | |
| num_cols, cat_cols = infer_options(df) | |
| all_cols = list(df.columns) | |
| x_default = "date" if "date" in all_cols else (cat_cols[0] if cat_cols else (num_cols[0] if num_cols else None)) | |
| y_default = num_cols[0] if num_cols else None | |
| return ( | |
| None, | |
| gr.Dropdown.update(choices=all_cols, value=x_default), | |
| gr.Dropdown.update(choices=num_cols, value=y_default), | |
| gr.Dropdown.update(choices=all_cols + [None], value=None), | |
| gr.Dropdown.update(choices=cat_cols + [None], value=None), | |
| df.head(10), | |
| ) | |
| refresh.click(ui_refresh_columns, inputs=[file], outputs=[x, y, color, facet_col, preview]) | |
| use_demo.click(set_demo, inputs=[use_demo], outputs=[file, x, y, color, facet_col, preview]) | |
| # Auto-refresh columns on file change | |
| file.change(ui_refresh_columns, inputs=[file], outputs=[x, y, color, facet_col, preview]) | |
| # Draw plot when any control changes | |
| controls = [file, plot_type, x, y, color, facet_col, agg, smooth, smooth_window, add_trend, trend_degree, points, theme, color_scale] | |
| for c in controls: | |
| c.change(ui_plot, inputs=controls, outputs=out) | |
| # Initial state using demo data | |
| _ = set_demo(None) | |
| out.update(value=build_plot(make_demo_data(), PlotConfig( | |
| plot_type="line", x="date", y="sales", color="region", facet_col=None, | |
| agg="mean", smooth=True, smooth_window=7, add_trend=False, trend_degree=1, | |
| points=False, theme="plotly_white", color_scale="Turbo" | |
| ))) | |
| if __name__ == "__main__": | |
| demo.launch() # You can set share=True when running locally to create a public link | |