import json from pathlib import Path import gradio as gr import pandas as pd import plotly.graph_objects as go import plotly.io as pio pio.templates.default = "plotly_white" DATA_PATH = Path("radar_data.json") def plot_spyder(scores: dict, title: str = "Group Chart", range_max: float | None = None) -> go.Figure: if not scores: raise ValueError("scores dict is empty") labels = list(scores.keys()) values = list(scores.values()) labels_loop = labels + [labels[0]] values_loop = values + [values[0]] fig = go.Figure( data=go.Scatterpolar( r=values_loop, theta=labels_loop, fill="toself", name=title, line=dict(width=3), ) ) fig.update_layout( title=title, polar=dict( radialaxis=dict( visible=True, range=[0, range_max if range_max is not None else max(values)], ) ), showlegend=False, ) return fig def build_scores(df: pd.DataFrame) -> dict: df = df.dropna(subset=["label", "value"]) df["label"] = df["label"].astype(str).str.strip() df = df[df["label"] != ""] return dict(zip(df["label"], df["value"].astype(float))) def load_state(path: Path): with path.open("r", encoding="utf-8") as f: raw = json.load(f) return raw def save_state(path: Path, payload: dict): with path.open("w", encoding="utf-8") as f: json.dump(payload, f, indent=2) def state_to_ui(state: dict): range_max = state.get("range_max", 5) charts = state.get("charts", []) def to_df(ch): return pd.DataFrame({"label": list(ch["scores"].keys()), "value": list(ch["scores"].values())}) df1 = to_df(charts[0]) df2 = to_df(charts[1]) df3 = to_df(charts[2]) df4 = to_df(charts[3]) t1 = charts[0]["title"] t2 = charts[1]["title"] t3 = charts[2]["title"] t4 = charts[3]["title"] return df1, t1, df2, t2, df3, t3, df4, t4, range_max def ui_to_state(df1, t1, df2, t2, df3, t3, df4, t4, range_max): rm = None if range_max <= 0 else range_max payload = { "range_max": range_max, "charts": [ {"title": t1, "scores": build_scores(df1)}, {"title": t2, "scores": build_scores(df2)}, {"title": t3, "scores": build_scores(df3)}, {"title": t4, "scores": build_scores(df4)}, ], } figs = ( plot_spyder(payload["charts"][0]["scores"], t1, rm), plot_spyder(payload["charts"][1]["scores"], t2, rm), plot_spyder(payload["charts"][2]["scores"], t3, rm), plot_spyder(payload["charts"][3]["scores"], t4, rm), ) save_state(DATA_PATH, payload) return (*figs, gr.update(value="Saved ✓")) # ---- App bootstrapping ---- if not DATA_PATH.exists(): raise FileNotFoundError(f"Missing {DATA_PATH}. Create it using the sample JSON below.") state = load_state(DATA_PATH) df1, t1, df2, t2, df3, t3, df4, t4, range_max = state_to_ui(state) with gr.Blocks() as demo: gr.Markdown("## Radar Chart Builder — All 4 Groups together") range_input = gr.Number(value=range_max, label="Shared Range Max (0 = auto)") with gr.Row(): with gr.Column(): df1_input = gr.Dataframe( value=df1, headers=["label", "value"], datatype=["str", "number"], row_count=(8, "dynamic"), column_count=(2, "fixed"), interactive=True, label="Chart 1 Data" ) t1_input = gr.Textbox(value=t1, label="Chart 1 Title") with gr.Column(): df2_input = gr.Dataframe( value=df2, headers=["label", "value"], datatype=["str", "number"], row_count=(8, "dynamic"), column_count=(2, "fixed"), interactive=True, label="Chart 2 Data" ) t2_input = gr.Textbox(value=t2, label="Chart 2 Title") with gr.Row(): with gr.Column(): df3_input = gr.Dataframe( value=df3, headers=["label", "value"], datatype=["str", "number"], row_count=(8, "dynamic"), column_count=(2, "fixed"), interactive=True, label="Chart 3 Data" ) t3_input = gr.Textbox(value=t3, label="Chart 3 Title") with gr.Column(): df4_input = gr.Dataframe( value=df4, headers=["label", "value"], datatype=["str", "number"], row_count=(8, "dynamic"), column_count=(2, "fixed"), interactive=True, label="Chart 4 Data" ) t4_input = gr.Textbox(value=t4, label="Chart 4 Title") save_plot_button = gr.Button("Save & Plot") status = gr.Textbox(value="", label="Status", interactive=False) with gr.Row(): out1 = gr.Plot(label="Chart 1") out2 = gr.Plot(label="Chart 2") with gr.Row(): out3 = gr.Plot(label="Chart 3") out4 = gr.Plot(label="Chart 4") save_plot_button.click( fn=ui_to_state, inputs=[df1_input, t1_input, df2_input, t2_input, df3_input, t3_input, df4_input, t4_input, range_input], outputs=[out1, out2, out3, out4, status], ) demo.launch()