Update app.py
Browse files
app.py
CHANGED
@@ -65,7 +65,7 @@ CSS = """
|
|
65 |
}
|
66 |
"""
|
67 |
|
68 |
-
# --- Helper Functions ---
|
69 |
def safe_exec(code_string: str, local_vars: dict):
|
70 |
output_buffer = io.StringIO()
|
71 |
try:
|
@@ -78,7 +78,6 @@ def safe_exec(code_string: str, local_vars: dict):
|
|
78 |
except Exception as e:
|
79 |
return None, None, None, f"Execution Error: {str(e)}"
|
80 |
|
81 |
-
# --- Core Data Processing & State Management ---
|
82 |
def load_and_process_file(file_obj, state_dict):
|
83 |
if file_obj is None: return state_dict, "Please upload a file.", *[gr.update(visible=False)] * 4
|
84 |
try:
|
@@ -111,23 +110,15 @@ def extract_dataset_metadata(df: pd.DataFrame):
|
|
111 |
return {'shape': (rows, cols), 'columns': df.columns.tolist(), 'numeric_cols': numeric_cols, 'categorical_cols': categorical_cols,
|
112 |
'datetime_cols': datetime_cols, 'dtypes': df.dtypes.to_string(), 'head': df.head().to_string(), 'data_quality': data_quality}
|
113 |
|
114 |
-
# --- Page Navigation ---
|
115 |
def switch_page(page_name):
|
116 |
return (gr.update(visible=page_name=="cockpit"), gr.update(visible=page_name=="deep_dive"), gr.update(visible=page_name=="co-pilot"))
|
117 |
|
118 |
-
# --- Page 1: Data Cockpit ---
|
119 |
def get_ai_suggestions(state_dict, api_key):
|
120 |
if not api_key: return "Enter your Gemini API key to get suggestions.", *[gr.update(visible=False)]*5
|
121 |
if not state_dict: return "Upload data first.", *[gr.update(visible=False)]*5
|
122 |
-
|
123 |
metadata = state_dict['metadata']
|
124 |
prompt = f"""
|
125 |
-
Based on the following dataset metadata, generate 3 to 5 specific, actionable, and interesting analytical questions
|
126 |
-
- **Columns:** {', '.join(metadata['columns'])}
|
127 |
-
- **Numeric:** {', '.join(metadata['numeric_cols'])}
|
128 |
-
- **Categorical:** {', '.join(metadata['categorical_cols'])}
|
129 |
-
- **Datetime:** {', '.join(metadata['datetime_cols'])}
|
130 |
-
|
131 |
Return ONLY a JSON list of strings. Example: ["What is the trend of sales over time?", "Which category has the highest average price?"]
|
132 |
"""
|
133 |
try:
|
@@ -138,22 +129,18 @@ def get_ai_suggestions(state_dict, api_key):
|
|
138 |
buttons = [gr.Button(s, variant="secondary", visible=True) for s in suggestions]
|
139 |
buttons += [gr.Button(visible=False)] * (5 - len(buttons))
|
140 |
return gr.update(visible=False), *buttons
|
141 |
-
except Exception as e:
|
142 |
-
return f"Could not generate suggestions: {e}", *[gr.update(visible=False)]*5
|
143 |
|
144 |
def handle_suggestion_click(question_text):
|
145 |
return (gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), question_text)
|
146 |
|
147 |
-
# --- Page 2: Deep Dive Dashboard ---
|
148 |
def add_plot_to_dashboard(state_dict, x_col, y_col, plot_type):
|
149 |
if not x_col:
|
150 |
gr.Warning("Please select at least an X-axis column.")
|
151 |
return state_dict, state_dict.get('dashboard_plots', [])
|
152 |
-
|
153 |
df = state_dict['df']
|
154 |
title = f"{plot_type.capitalize()}: {y_col} by {x_col}" if y_col else f"Distribution of {x_col}"
|
155 |
fig = None
|
156 |
-
|
157 |
try:
|
158 |
if plot_type == 'histogram': fig = px.histogram(df, x=x_col, title=title)
|
159 |
elif plot_type == 'box': fig = px.box(df, x=x_col, y=y_col, title=title)
|
@@ -162,12 +149,9 @@ def add_plot_to_dashboard(state_dict, x_col, y_col, plot_type):
|
|
162 |
counts = df[x_col].value_counts().nlargest(20)
|
163 |
fig = px.bar(counts, x=counts.index, y=counts.values, title=f"Top 20 Categories for {x_col}")
|
164 |
fig.update_xaxes(title=x_col)
|
165 |
-
|
166 |
if fig:
|
167 |
fig.update_layout(template="plotly_dark")
|
168 |
state_dict['dashboard_plots'].append(fig)
|
169 |
-
|
170 |
-
# CORRECTED: Return the list of plots to the Gallery component
|
171 |
return state_dict, state_dict['dashboard_plots']
|
172 |
except Exception as e:
|
173 |
gr.Warning(f"Plotting Error: {e}")
|
@@ -175,18 +159,15 @@ def add_plot_to_dashboard(state_dict, x_col, y_col, plot_type):
|
|
175 |
|
176 |
def clear_dashboard(state_dict):
|
177 |
state_dict['dashboard_plots'] = []
|
178 |
-
# CORRECTED: Return an empty list to clear the Gallery
|
179 |
return state_dict, []
|
180 |
|
181 |
-
# --- Page 3: AI Co-pilot ---
|
182 |
def respond_to_chat(user_message, history, state_dict, api_key):
|
183 |
if not api_key:
|
184 |
-
history.append((user_message, "I need a Gemini API key to function
|
185 |
return history, *[gr.update(visible=False)] * 4
|
186 |
if not state_dict:
|
187 |
history.append((user_message, "Please upload a dataset first."))
|
188 |
return history, *[gr.update(visible=False)] * 4
|
189 |
-
|
190 |
history.append((user_message, None))
|
191 |
metadata = state_dict['metadata']
|
192 |
prompt = f"""
|
@@ -203,9 +184,7 @@ def respond_to_chat(user_message, history, state_dict, api_key):
|
|
203 |
thought = response_json.get("thought", "Thinking...")
|
204 |
code_to_run = response_json.get("code", "")
|
205 |
explanation = response_json.get("explanation", "Here is the result.")
|
206 |
-
|
207 |
stdout, fig_result, df_result, error = safe_exec(code_to_run, {'df': state_dict['df'], 'px': px, 'pd': pd, 'np': np})
|
208 |
-
|
209 |
history[-1] = (user_message, f"π€ **Thought:** *{thought}*")
|
210 |
|
211 |
output_updates = [gr.update(visible=False, value=None)] * 4
|
@@ -218,10 +197,9 @@ def respond_to_chat(user_message, history, state_dict, api_key):
|
|
218 |
output_updates[0] = gr.update(visible=True, value=new_explanation)
|
219 |
if error:
|
220 |
output_updates[0] = gr.update(visible=True, value=f"**Phoenix Co-pilot:** I encountered an error. Here's the details:\n\n`{error}`")
|
221 |
-
|
222 |
return history, *output_updates
|
223 |
except Exception as e:
|
224 |
-
history[-1] = (user_message, f"A critical error occurred: {e}.
|
225 |
return history, *[gr.update(visible=False)] * 4
|
226 |
|
227 |
# --- Gradio UI Definition ---
|
@@ -229,58 +207,105 @@ def create_gradio_interface():
|
|
229 |
with gr.Blocks(theme=gr.themes.Glass(primary_hue="indigo", secondary_hue="blue"), css=CSS, title="Phoenix AI Data Explorer") as demo:
|
230 |
global_state = gr.State({})
|
231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
232 |
with gr.Row():
|
233 |
-
# Sidebar
|
234 |
with gr.Column(scale=1, elem_classes="sidebar"):
|
235 |
gr.Markdown("## π Phoenix UI")
|
236 |
-
cockpit_btn
|
237 |
-
deep_dive_btn
|
238 |
-
copilot_btn
|
239 |
gr.Markdown("---")
|
240 |
-
file_input
|
241 |
-
status_output
|
242 |
gr.Markdown("---")
|
243 |
-
api_key_input
|
244 |
-
suggestion_btn
|
245 |
|
246 |
-
# Main Content
|
247 |
with gr.Column(scale=4):
|
248 |
with gr.Column(visible=True) as welcome_page:
|
249 |
gr.Markdown("# Welcome to the AI Data Explorer (Phoenix UI)")
|
250 |
gr.Markdown("Please **upload a CSV file** and **enter your Gemini API key** in the sidebar to begin.")
|
251 |
-
gr.Image(value="workflow.png",
|
252 |
|
253 |
with gr.Column(visible=False) as cockpit_page:
|
254 |
gr.Markdown("## π Data Cockpit")
|
255 |
-
|
256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
257 |
with gr.Accordion(label="β¨ AI Smart Suggestions", open=True):
|
258 |
-
|
|
|
259 |
|
260 |
with gr.Column(visible=False) as deep_dive_page:
|
261 |
gr.Markdown("## π Deep Dive Dashboard Builder")
|
262 |
gr.Markdown("Create a custom dashboard by adding multiple plots to the gallery below.")
|
263 |
with gr.Row():
|
264 |
-
plot_type_dd
|
265 |
-
x_col_dd = gr.Dropdown([], label="X-Axis / Column")
|
266 |
-
y_col_dd = gr.Dropdown([], label="Y-Axis (for Scatter/Box)")
|
267 |
with gr.Row():
|
268 |
-
add_plot_btn
|
269 |
-
|
270 |
-
|
271 |
-
# CORRECTED: Replaced Accordion with Gallery
|
272 |
-
dashboard_gallery = gr.Gallery(label="π Your Custom Dashboard", height="auto", columns=2)
|
273 |
|
274 |
with gr.Column(visible=False) as copilot_page:
|
275 |
gr.Markdown("## π€ AI Co-pilot")
|
276 |
-
|
|
|
|
|
|
|
|
|
277 |
|
278 |
-
#
|
279 |
pages = [cockpit_page, deep_dive_page, copilot_page]
|
280 |
nav_buttons = [cockpit_btn, deep_dive_btn, copilot_btn]
|
281 |
|
282 |
for i, btn in enumerate(nav_buttons):
|
283 |
-
|
|
|
284 |
.then(lambda i=i: [gr.update(elem_classes="selected" if j==i else "") for j in range(len(nav_buttons))], outputs=nav_buttons)
|
285 |
|
286 |
file_input.upload(load_and_process_file, [file_input, global_state],
|
@@ -296,7 +321,6 @@ def create_gradio_interface():
|
|
296 |
btn.click(handle_suggestion_click, inputs=[btn], outputs=[cockpit_page, deep_dive_page, copilot_page, chat_input]) \
|
297 |
.then(lambda: (gr.update(elem_classes=""), gr.update(elem_classes=""), gr.update(elem_classes="selected")), outputs=nav_buttons)
|
298 |
|
299 |
-
# CORRECTED: Event handlers now output to the gallery
|
300 |
add_plot_btn.click(add_plot_to_dashboard, [global_state, x_col_dd, y_col_dd, plot_type_dd], [global_state, dashboard_gallery])
|
301 |
clear_plots_btn.click(clear_dashboard, [global_state], [global_state, dashboard_gallery])
|
302 |
|
|
|
65 |
}
|
66 |
"""
|
67 |
|
68 |
+
# --- Helper and Core Functions (Unchanged) ---
|
69 |
def safe_exec(code_string: str, local_vars: dict):
|
70 |
output_buffer = io.StringIO()
|
71 |
try:
|
|
|
78 |
except Exception as e:
|
79 |
return None, None, None, f"Execution Error: {str(e)}"
|
80 |
|
|
|
81 |
def load_and_process_file(file_obj, state_dict):
|
82 |
if file_obj is None: return state_dict, "Please upload a file.", *[gr.update(visible=False)] * 4
|
83 |
try:
|
|
|
110 |
return {'shape': (rows, cols), 'columns': df.columns.tolist(), 'numeric_cols': numeric_cols, 'categorical_cols': categorical_cols,
|
111 |
'datetime_cols': datetime_cols, 'dtypes': df.dtypes.to_string(), 'head': df.head().to_string(), 'data_quality': data_quality}
|
112 |
|
|
|
113 |
def switch_page(page_name):
|
114 |
return (gr.update(visible=page_name=="cockpit"), gr.update(visible=page_name=="deep_dive"), gr.update(visible=page_name=="co-pilot"))
|
115 |
|
|
|
116 |
def get_ai_suggestions(state_dict, api_key):
|
117 |
if not api_key: return "Enter your Gemini API key to get suggestions.", *[gr.update(visible=False)]*5
|
118 |
if not state_dict: return "Upload data first.", *[gr.update(visible=False)]*5
|
|
|
119 |
metadata = state_dict['metadata']
|
120 |
prompt = f"""
|
121 |
+
Based on the following dataset metadata, generate 3 to 5 specific, actionable, and interesting analytical questions...
|
|
|
|
|
|
|
|
|
|
|
122 |
Return ONLY a JSON list of strings. Example: ["What is the trend of sales over time?", "Which category has the highest average price?"]
|
123 |
"""
|
124 |
try:
|
|
|
129 |
buttons = [gr.Button(s, variant="secondary", visible=True) for s in suggestions]
|
130 |
buttons += [gr.Button(visible=False)] * (5 - len(buttons))
|
131 |
return gr.update(visible=False), *buttons
|
132 |
+
except Exception as e: return f"Could not generate suggestions: {e}", *[gr.update(visible=False)]*5
|
|
|
133 |
|
134 |
def handle_suggestion_click(question_text):
|
135 |
return (gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), question_text)
|
136 |
|
|
|
137 |
def add_plot_to_dashboard(state_dict, x_col, y_col, plot_type):
|
138 |
if not x_col:
|
139 |
gr.Warning("Please select at least an X-axis column.")
|
140 |
return state_dict, state_dict.get('dashboard_plots', [])
|
|
|
141 |
df = state_dict['df']
|
142 |
title = f"{plot_type.capitalize()}: {y_col} by {x_col}" if y_col else f"Distribution of {x_col}"
|
143 |
fig = None
|
|
|
144 |
try:
|
145 |
if plot_type == 'histogram': fig = px.histogram(df, x=x_col, title=title)
|
146 |
elif plot_type == 'box': fig = px.box(df, x=x_col, y=y_col, title=title)
|
|
|
149 |
counts = df[x_col].value_counts().nlargest(20)
|
150 |
fig = px.bar(counts, x=counts.index, y=counts.values, title=f"Top 20 Categories for {x_col}")
|
151 |
fig.update_xaxes(title=x_col)
|
|
|
152 |
if fig:
|
153 |
fig.update_layout(template="plotly_dark")
|
154 |
state_dict['dashboard_plots'].append(fig)
|
|
|
|
|
155 |
return state_dict, state_dict['dashboard_plots']
|
156 |
except Exception as e:
|
157 |
gr.Warning(f"Plotting Error: {e}")
|
|
|
159 |
|
160 |
def clear_dashboard(state_dict):
|
161 |
state_dict['dashboard_plots'] = []
|
|
|
162 |
return state_dict, []
|
163 |
|
|
|
164 |
def respond_to_chat(user_message, history, state_dict, api_key):
|
165 |
if not api_key:
|
166 |
+
history.append((user_message, "I need a Gemini API key to function..."))
|
167 |
return history, *[gr.update(visible=False)] * 4
|
168 |
if not state_dict:
|
169 |
history.append((user_message, "Please upload a dataset first."))
|
170 |
return history, *[gr.update(visible=False)] * 4
|
|
|
171 |
history.append((user_message, None))
|
172 |
metadata = state_dict['metadata']
|
173 |
prompt = f"""
|
|
|
184 |
thought = response_json.get("thought", "Thinking...")
|
185 |
code_to_run = response_json.get("code", "")
|
186 |
explanation = response_json.get("explanation", "Here is the result.")
|
|
|
187 |
stdout, fig_result, df_result, error = safe_exec(code_to_run, {'df': state_dict['df'], 'px': px, 'pd': pd, 'np': np})
|
|
|
188 |
history[-1] = (user_message, f"π€ **Thought:** *{thought}*")
|
189 |
|
190 |
output_updates = [gr.update(visible=False, value=None)] * 4
|
|
|
197 |
output_updates[0] = gr.update(visible=True, value=new_explanation)
|
198 |
if error:
|
199 |
output_updates[0] = gr.update(visible=True, value=f"**Phoenix Co-pilot:** I encountered an error. Here's the details:\n\n`{error}`")
|
|
|
200 |
return history, *output_updates
|
201 |
except Exception as e:
|
202 |
+
history[-1] = (user_message, f"A critical error occurred: {e}.")
|
203 |
return history, *[gr.update(visible=False)] * 4
|
204 |
|
205 |
# --- Gradio UI Definition ---
|
|
|
207 |
with gr.Blocks(theme=gr.themes.Glass(primary_hue="indigo", secondary_hue="blue"), css=CSS, title="Phoenix AI Data Explorer") as demo:
|
208 |
global_state = gr.State({})
|
209 |
|
210 |
+
# --- CORRECTED: "Define-Then-Place" Pattern ---
|
211 |
+
# 1. DEFINE all interactive components first.
|
212 |
+
|
213 |
+
# Sidebar components
|
214 |
+
cockpit_btn = gr.Button("π Data Cockpit", elem_classes="selected")
|
215 |
+
deep_dive_btn = gr.Button("π Deep Dive Builder")
|
216 |
+
copilot_btn = gr.Button("π€ AI Co-pilot")
|
217 |
+
file_input = gr.File(label="π Upload New CSV", file_types=[".csv"])
|
218 |
+
status_output = gr.Markdown("Status: Awaiting data...")
|
219 |
+
api_key_input = gr.Textbox(label="π Gemini API Key", type="password", placeholder="Enter key here...")
|
220 |
+
suggestion_btn = gr.Button("Get Smart Suggestions", variant="secondary")
|
221 |
+
|
222 |
+
# Cockpit page components
|
223 |
+
rows_stat = gr.Textbox("0", show_label=False, elem_classes="stat-card-value", interactive=False)
|
224 |
+
cols_stat = gr.Textbox("0", show_label=False, elem_classes="stat-card-value", interactive=False)
|
225 |
+
quality_stat = gr.Textbox("0%", show_label=False, elem_classes="stat-card-value", interactive=False)
|
226 |
+
time_cols_stat = gr.Textbox("0", show_label=False, elem_classes="stat-card-value", interactive=False)
|
227 |
+
suggestion_status = gr.Markdown(visible=True)
|
228 |
+
suggestion_buttons = [gr.Button(visible=False) for _ in range(5)]
|
229 |
+
|
230 |
+
# Deep Dive page components
|
231 |
+
plot_type_dd = gr.Dropdown(['histogram', 'bar', 'scatter', 'box'], label="Plot Type", value='histogram')
|
232 |
+
x_col_dd = gr.Dropdown([], label="X-Axis / Column")
|
233 |
+
y_col_dd = gr.Dropdown([], label="Y-Axis (for Scatter/Box)")
|
234 |
+
add_plot_btn = gr.Button("Add to Dashboard", variant="primary")
|
235 |
+
clear_plots_btn = gr.Button("Clear Dashboard")
|
236 |
+
dashboard_gallery = gr.Gallery(label="π Your Custom Dashboard", height="auto", columns=2, preview=True)
|
237 |
+
|
238 |
+
# Co-pilot page components
|
239 |
+
chatbot = gr.Chatbot(height=400, label="Conversation with Co-pilot", show_copy_button=True)
|
240 |
+
copilot_explanation = gr.Markdown(visible=False, elem_classes="explanation-block")
|
241 |
+
copilot_code = gr.Code(language="python", visible=False, label="Executed Python Code")
|
242 |
+
copilot_plot = gr.Plot(visible=False, label="Generated Visualization")
|
243 |
+
copilot_table = gr.Dataframe(visible=False, label="Generated Table", wrap=True)
|
244 |
+
chat_input = gr.Textbox(label="Your Question", placeholder="e.g., 'What is the correlation between age and salary?'", scale=4)
|
245 |
+
chat_submit_btn = gr.Button("Submit", variant="primary")
|
246 |
+
|
247 |
+
# 2. PLACE the defined components into the layout.
|
248 |
with gr.Row():
|
249 |
+
# Sidebar Layout
|
250 |
with gr.Column(scale=1, elem_classes="sidebar"):
|
251 |
gr.Markdown("## π Phoenix UI")
|
252 |
+
cockpit_btn
|
253 |
+
deep_dive_btn
|
254 |
+
copilot_btn
|
255 |
gr.Markdown("---")
|
256 |
+
file_input
|
257 |
+
status_output
|
258 |
gr.Markdown("---")
|
259 |
+
api_key_input
|
260 |
+
suggestion_btn
|
261 |
|
262 |
+
# Main Content Layout
|
263 |
with gr.Column(scale=4):
|
264 |
with gr.Column(visible=True) as welcome_page:
|
265 |
gr.Markdown("# Welcome to the AI Data Explorer (Phoenix UI)")
|
266 |
gr.Markdown("Please **upload a CSV file** and **enter your Gemini API key** in the sidebar to begin.")
|
267 |
+
gr.Image(value="workflow.png", show_label=False, show_download_button=False, container=False)
|
268 |
|
269 |
with gr.Column(visible=False) as cockpit_page:
|
270 |
gr.Markdown("## π Data Cockpit")
|
271 |
+
with gr.Row():
|
272 |
+
with gr.Column(elem_classes="stat-card"):
|
273 |
+
gr.Markdown("<div class='stat-card-title'>Rows</div>"); rows_stat
|
274 |
+
with gr.Column(elem_classes="stat-card"):
|
275 |
+
gr.Markdown("<div class='stat-card-title'>Columns</div>"); cols_stat
|
276 |
+
with gr.Column(elem_classes="stat-card"):
|
277 |
+
gr.Markdown("<div class='stat-card-title'>Data Quality</div>"); quality_stat
|
278 |
+
with gr.Column(elem_classes="stat-card"):
|
279 |
+
gr.Markdown("<div class='stat-card-title'>Date/Time Cols</div>"); time_cols_stat
|
280 |
+
suggestion_status
|
281 |
with gr.Accordion(label="β¨ AI Smart Suggestions", open=True):
|
282 |
+
for btn in suggestion_buttons:
|
283 |
+
btn
|
284 |
|
285 |
with gr.Column(visible=False) as deep_dive_page:
|
286 |
gr.Markdown("## π Deep Dive Dashboard Builder")
|
287 |
gr.Markdown("Create a custom dashboard by adding multiple plots to the gallery below.")
|
288 |
with gr.Row():
|
289 |
+
plot_type_dd; x_col_dd; y_col_dd
|
|
|
|
|
290 |
with gr.Row():
|
291 |
+
add_plot_btn; clear_plots_btn
|
292 |
+
dashboard_gallery
|
|
|
|
|
|
|
293 |
|
294 |
with gr.Column(visible=False) as copilot_page:
|
295 |
gr.Markdown("## π€ AI Co-pilot")
|
296 |
+
chatbot
|
297 |
+
with gr.Accordion("Co-pilot's Response Details", open=True):
|
298 |
+
copilot_explanation; copilot_code; copilot_plot; copilot_table
|
299 |
+
with gr.Row():
|
300 |
+
chat_input; chat_submit_btn
|
301 |
|
302 |
+
# 3. DEFINE all event handlers. Now all component variables are guaranteed to exist.
|
303 |
pages = [cockpit_page, deep_dive_page, copilot_page]
|
304 |
nav_buttons = [cockpit_btn, deep_dive_btn, copilot_btn]
|
305 |
|
306 |
for i, btn in enumerate(nav_buttons):
|
307 |
+
page_name = btn.value.lower().replace(" ", "_").split(" ")[-1]
|
308 |
+
btn.click(lambda name=page_name: switch_page(name), outputs=pages) \
|
309 |
.then(lambda i=i: [gr.update(elem_classes="selected" if j==i else "") for j in range(len(nav_buttons))], outputs=nav_buttons)
|
310 |
|
311 |
file_input.upload(load_and_process_file, [file_input, global_state],
|
|
|
321 |
btn.click(handle_suggestion_click, inputs=[btn], outputs=[cockpit_page, deep_dive_page, copilot_page, chat_input]) \
|
322 |
.then(lambda: (gr.update(elem_classes=""), gr.update(elem_classes=""), gr.update(elem_classes="selected")), outputs=nav_buttons)
|
323 |
|
|
|
324 |
add_plot_btn.click(add_plot_to_dashboard, [global_state, x_col_dd, y_col_dd, plot_type_dd], [global_state, dashboard_gallery])
|
325 |
clear_plots_btn.click(clear_dashboard, [global_state], [global_state, dashboard_gallery])
|
326 |
|