Spaces:
Sleeping
Sleeping
import os | |
import json | |
import math | |
import plotly.graph_objects as go | |
from plotly.subplots import make_subplots | |
import dash | |
from dash import dcc, html, Input, Output, State, exceptions, no_update | |
# Assume name is defined, e.g., name = 'main' | |
try: | |
name | |
except NameError: | |
name = '__main__' # Define name if not running directly | |
app = dash.Dash(name) | |
# ================== 数据准备 (Data Preparation) ================== | |
# (Keep the existing data preparation code) | |
# Define the path relative to the script's location | |
# Use os.path.join for better cross-platform compatibility | |
base_dir = os.path.dirname(os.path.abspath(__file__)) if '__file__' in globals() else '.' | |
results_dir = os.path.join(base_dir, "test_results_report") | |
file_names = [ | |
'GPT-4o_statistics.txt', | |
'GPT-4o-mini_statistics.txt', | |
'Llama-3.1-8B-ft_statistics.txt', | |
'Llama-3.1-8B_statistics.txt', | |
'Llama-3.1-70B_statistics.txt', | |
'Mixtral-8x7B_statistics.txt', | |
'Qwen2-72B_statistics.txt', | |
'Qwen2-7B_statistics.txt', | |
'Llama-2-7b-hf_statistics.txt', | |
'Llama-2-7b-hf-5-shot_statistics.txt', | |
'Llama-3.1-8B-5-shot_statistics.txt', | |
'Llama-3.1-70B-5-shot_statistics.txt', | |
'Mixtral-8x7B-v0.1-5-shot_statistics.txt', | |
'Qwen2-7B-5-shot_statistics.txt', | |
'Qwen2-72B-5-shot_statistics.txt', | |
] | |
# Ensure the directory and at least one file exists for key loading | |
if not os.path.exists(results_dir): | |
print(f"Error: Results directory not found at {results_dir}") | |
# Consider exiting or providing default keys if essential | |
keys = [] # Provide empty keys as fallback | |
else: | |
first_file_path = os.path.join(results_dir, file_names[0] if file_names else '') | |
if not file_names or not os.path.exists(first_file_path): | |
print(f"Warning: First statistics file not found or file_names list is empty.") | |
keys = [] # Provide empty keys as fallback | |
else: | |
# 从一个文件获取全部key, 仅用于演示 (Get all keys from one file, for demo only) | |
try: | |
with open(first_file_path, "r") as f: | |
results = json.load(f) | |
# Ensure 'exact_match' exists before accessing keys | |
if "exact_match" in results and isinstance(results["exact_match"], dict): | |
keys = list(results["exact_match"].keys()) | |
else: | |
print(f"Warning: 'exact_match' key not found or not a dictionary in {first_file_path}. Setting keys to empty.") | |
keys = [] | |
except Exception as e: | |
print(f"Error loading keys from {first_file_path}: {e}") | |
keys = [] # Provide empty keys on error | |
def load_data(file_name, main_metric="exact_match", r=(0, len(keys))): | |
# --- Keep existing load_data function --- | |
tasks = [] | |
well_learned_digit = [] | |
has_performance_digit = [] | |
in_domain = [] | |
out_domain = [] | |
short_range = [] | |
medium_range = [] | |
long_range = [] | |
very_long_range = [] | |
metrics_to_extract = [ | |
"well_learned_digit", "has_performance_digit", "in_domain", "out_domain", | |
"short_range", "medium_range", "long_range", "very_long_range" | |
] | |
file_path = os.path.join(results_dir, file_name) | |
if not os.path.exists(file_path): | |
print(f"Warning: File not found {file_path}, skipping.") | |
# Return empty lists of the correct structure | |
return tuple([[] for _ in range(len(metrics_to_extract) + 1)]) | |
try: | |
with open(file_path, "r") as f: | |
stats = json.load(f) | |
except Exception as e: | |
print(f"Error reading or parsing {file_path}: {e}") | |
# Return empty lists of the correct structure | |
return tuple([[] for _ in range(len(metrics_to_extract) + 1)]) | |
if main_metric not in stats: | |
print(f"Warning: Metric '{main_metric}' not found in {file_name}, skipping.") | |
return tuple([[] for _ in range(len(metrics_to_extract) + 1)]) | |
stats_exm = stats[main_metric] | |
# Ensure stats_exm is a dictionary before proceeding | |
if not isinstance(stats_exm, dict): | |
print(f"Warning: Metric '{main_metric}' in {file_name} is not a dictionary. Skipping.") | |
return tuple([[] for _ in range(len(metrics_to_extract) + 1)]) | |
# Check if keys exist before accessing | |
valid_keys = [k for k in keys[r[0]:r[1]] if k in stats_exm] | |
if len(valid_keys) < len(keys[r[0]:r[1]]): | |
print(f"Warning: Some keys not found in {main_metric} of {file_name}") | |
for key in valid_keys: # Use only valid keys | |
try: | |
words = key.split("_") | |
if len(words) < 4: # Basic check for expected format | |
print(f"Warning: Unexpected key format '{key}' in {file_name}, skipping.") | |
continue | |
domain_3 = words.pop() | |
domain_2 = words.pop() | |
domain_1 = words.pop() | |
task = " ".join(list(map(str.capitalize, words))) | |
tasks.append(f"{task}<br />{domain_1}") | |
key_data = stats_exm[key] | |
# Ensure key_data is a dictionary | |
if not isinstance(key_data, dict): | |
print(f"Warning: Data for key '{key}' in {file_name} is not a dictionary. Appending zeros.") | |
well_learned_digit.append(0) | |
has_performance_digit.append(0) | |
in_domain.append(0) | |
out_domain.append(0) | |
short_range.append(0) | |
medium_range.append(0) | |
long_range.append(0) | |
very_long_range.append(0) | |
continue # Skip to next key | |
# Safely append metrics, using None or 0 if a metric is missing for a specific key | |
well_learned_digit.append(key_data.get("well_learned_digit", 0)) | |
has_performance_digit.append(key_data.get("has_performance_digit", 0)) | |
in_domain.append(key_data.get("in_domain", 0)) | |
out_domain.append(key_data.get("out_domain", 0)) | |
short_range.append(key_data.get("short_range", 0)) | |
medium_range.append(key_data.get("medium_range", 0)) | |
long_range.append(key_data.get("long_range", 0)) | |
very_long_range.append(key_data.get("very_long_range", 0)) | |
except Exception as e: | |
print(f"Error processing key '{key}' in {file_name}: {e}") | |
# Attempt to keep lists aligned by appending a default value, or skip the key entirely | |
# For simplicity, let's skip if parsing fails badly | |
if len(tasks) > len(short_range): # Check if appending failed mid-key | |
try: | |
tasks.pop() # Remove the task name if metrics failed | |
except IndexError: | |
pass # Handle case where tasks might be empty | |
return ( | |
tasks, well_learned_digit, has_performance_digit, | |
in_domain, out_domain, | |
short_range, medium_range, long_range, very_long_range | |
) | |
# 示例任务 (Example Tasks) | |
# (Keep existing task lists) | |
intTasks = [ | |
"Add","Sub","Max","Max Hard","Multiply Hard","Multiply Easy", | |
"Digit Max","Digit Add","Get Digit","Length","Truediv","Floordiv","Mod", | |
"Mod Easy","Count","Sig","To Scient" | |
] | |
floatTasks = [ | |
"Add","Sub","Max","Max Hard","Multiply Hard","Multiply Easy", | |
"Digit Max","Digit Add","Get Digit","Length","To Scient" | |
] | |
fractionTasks = [ | |
"Add","Add Easy","Sub","Max","Multiply Hard","Multiply Easy","Truediv","To Float" | |
] | |
sciTasks = [ | |
"Add","Sub","Max","Max Hard","Multiply Hard","Multiply Easy","To Float" | |
] | |
# ================== 核心绘图函数 (Core Plotting Function) ================== | |
# (Keep the corrected plot function from the previous step) | |
def plot(main_metric, selected_files, selected_metrics, selected_tasks, r): | |
colors = [ | |
"#2C6344","#5F9C61","#A4C97C","#61496D","#B092B6", | |
"#CAC1D4","#308192","#E38D26","#F1CC74","#C74D26", | |
"#5EA7B8","#AED2E2" | |
] | |
colors.reverse() | |
M = len(selected_files) | |
T = len(selected_tasks) | |
if M == 0 or T == 0 or not selected_metrics: # Added check for selected_metrics | |
fig = go.Figure() | |
fig.update_layout( | |
title="Please select models, tasks, and at least one range.", | |
xaxis={'visible': False}, | |
yaxis={'visible': False}, | |
annotations=[{ | |
'text': "No data to display. Check selections.", | |
'xref': "paper", | |
'yref': "paper", | |
'showarrow': False, | |
'font': {'size': 16} | |
}] | |
) | |
return fig | |
total_items = M * T | |
# Moved metric labels definition here - it doesn't depend on loaded data | |
metric_labels = { # For x-axis labels | |
"short_range": "S", | |
"medium_range": "M", | |
"long_range": "L", | |
"very_long_range": "XL", | |
# Add other potential metrics if they can be selected and need labels | |
"well_learned_digit": "WLD", | |
"has_performance_digit": "HPD", | |
"in_domain": "ID", | |
"out_domain": "OD" | |
} | |
# Filter labels based on *actually selected* metrics (ranges in this case) | |
selected_metric_labels = [metric_labels.get(m, m.replace('_',' ').title()) for m in selected_metrics] # Get labels for selected metrics | |
# ------ 单行模式 (Single Row Mode) ------ | |
if total_items <= 32: | |
fig = go.Figure() | |
# Keep track of unique task labels added for x-axis update | |
all_tasks_x_level1 = [] | |
for idx, file_name in enumerate(selected_files): | |
data_tuple = load_data(file_name, main_metric=main_metric, r=r) | |
# Unpack safely, ensuring enough elements exist | |
if len(data_tuple) < 9: continue # Skip if data loading failed badly | |
( | |
tasks, well_learned_digit, has_performance_digit, | |
in_domain, out_domain, | |
short_range, medium_range, long_range, very_long_range | |
) = data_tuple | |
# *** Define metric_vars HERE, AFTER load_data *** | |
metric_vars = { | |
"well_learned_digit": well_learned_digit, | |
"has_performance_digit": has_performance_digit, | |
"in_domain": in_domain, | |
"out_domain": out_domain, | |
"short_range": short_range, | |
"medium_range": medium_range, | |
"long_range": long_range, | |
"very_long_range": very_long_range | |
} | |
tasks_new_x_level1 = [] | |
tasks_old_indices = [] # Keep track of original indices for data lookup | |
performance = [] | |
for i, task in enumerate(tasks): | |
if task in selected_tasks: | |
tasks_new_x_level1.extend([task] * len(selected_metrics)) | |
tasks_old_indices.append(i) # Store index i | |
for sel_m in selected_metrics: | |
# Use the metric_vars map and the stored index i | |
metric_data_list = metric_vars.get(sel_m) | |
# Ensure metric_data_list is not None and index is valid | |
if metric_data_list is not None and i < len(metric_data_list): | |
performance.append(metric_data_list[i]) | |
else: | |
# Append None or 0 if metric data is missing for this index/metric | |
performance.append(None) | |
print(f"Warning: Missing data for metric '{sel_m}' at index {i} in file {file_name}") | |
if not tasks_new_x_level1: continue # Skip if no tasks matched for this file | |
# Store unique tasks added in this trace for later x-axis update | |
if idx == 0: # Only need tasks from the first file usually | |
all_tasks_x_level1 = tasks_new_x_level1 | |
# Ensure performance list length matches x-axis length | |
expected_len = len(tasks_new_x_level1) | |
if len(performance) != expected_len: | |
print(f"Warning: Length mismatch for {file_name}. X-axis: {expected_len}, Y-axis: {len(performance)}. Skipping trace.") | |
continue | |
# Ensure the second level of x-axis also matches | |
x_level2 = selected_metric_labels * (len(tasks_new_x_level1) // len(selected_metric_labels)) | |
if len(tasks_new_x_level1) != len(x_level2): | |
print(f"Warning: X-axis level length mismatch for {file_name}. Level 1: {len(tasks_new_x_level1)}, Level 2: {len(x_level2)}. Skipping trace.") | |
continue | |
fig.add_trace(go.Bar( | |
x=[tasks_new_x_level1, x_level2 ], | |
y=performance, | |
name=file_name[:-15] if file_name.endswith('_statistics.txt') else file_name, # Safer name slicing | |
marker_color=colors[idx % len(colors)], | |
legendgroup=f"legend_{idx}", # Group legend items | |
offsetgroup=f"group_{idx}" # Group bars | |
)) | |
# Single row layout updates | |
fig.update_layout( | |
barmode='group', | |
xaxis_tickangle=-45, | |
template="ggplot2", | |
autosize=True, | |
height=450, | |
xaxis=dict(showgrid=False, title='Task<br />Range', type='multicategory'), # Specify multicategory type | |
yaxis=dict(title='Performance'), | |
title=" ".join(list(map(str.capitalize, main_metric.split("_")))), | |
margin=dict(l=60, r=40, t=80, b=100), | |
legend_title_text='Models' | |
) | |
# Update x-axis ticks explicitly for multicategory if data exists | |
if all_tasks_x_level1 and selected_metric_labels: | |
unique_tasks = sorted(list(set(all_tasks_x_level1)), key=all_tasks_x_level1.index) # Get unique tasks in order | |
tickvals_level1 = [task for task in unique_tasks] | |
tickvals_level2 = [selected_metric_labels[0]] * len(unique_tasks) # Show only first metric label below task group | |
# This approach might still be tricky, Plotly's auto-labeling is often better for multicat | |
# Let's rely on Plotly's default multicategory labeling unless specific formatting is needed. | |
pass # Remove explicit tick setting unless necessary | |
return fig | |
else: | |
# ------ 多行模式 (Multi Row Mode) ------ | |
# (Keep the multi-row logic from the previous step, ensuring metric_vars is defined inside the inner loop) | |
row_count = math.ceil(total_items / 32) | |
row_height = 200 | |
gap_px = 100 | |
margin_top = 80 | |
margin_bottom = 80 | |
margin_left = 60 | |
margin_right = 40 | |
base_count = T // row_count | |
remainder = T % row_count | |
task_groups = [] | |
start_idx = 0 | |
for i in range(row_count): | |
this_count = base_count + (1 if i < remainder else 0) | |
group = selected_tasks[start_idx : start_idx + this_count] | |
task_groups.append(group) | |
start_idx += this_count | |
task_groups = [group for group in task_groups if group] | |
if not task_groups: | |
fig = go.Figure() | |
fig.update_layout(title="No tasks selected or data available for multi-row mode.") | |
return fig | |
row_count = len(task_groups) | |
net_height = row_count * row_height + max(0, row_count - 1) * gap_px | |
total_fig_height = net_height + margin_top + margin_bottom | |
vertical_spacing = gap_px / net_height if net_height > 0 and row_count > 1 else 0 | |
row_fraction = row_height / net_height if net_height > 0 else 1 | |
row_heights = [row_fraction] * row_count | |
fig = make_subplots( | |
rows=row_count, | |
cols=1, | |
row_heights=row_heights if row_heights else None, | |
vertical_spacing=vertical_spacing, | |
subplot_titles=[f"Tasks Subset {i+1}" for i in range(row_count)], | |
shared_xaxes=False | |
) | |
added_traces_info = {} | |
for row_i, sub_tasks in enumerate(task_groups, start=1): | |
if not sub_tasks: continue | |
row_x_level1 = [] | |
row_x_level2 = [] | |
for idx, file_name in enumerate(selected_files): | |
data_tuple = load_data(file_name, main_metric=main_metric, r=r) | |
if len(data_tuple) < 9: continue | |
( | |
tasks, well_learned_digit, has_performance_digit, | |
in_domain, out_domain, | |
short_range, medium_range, long_range, very_long_range | |
) = data_tuple | |
# *** Define metric_vars HERE *** | |
metric_vars = { | |
"well_learned_digit": well_learned_digit, | |
"has_performance_digit": has_performance_digit, | |
"in_domain": in_domain, | |
"out_domain": out_domain, | |
"short_range": short_range, | |
"medium_range": medium_range, | |
"long_range": long_range, | |
"very_long_range": very_long_range | |
} | |
tasks_new_x_level1 = [] | |
tasks_old_indices = [] | |
performance = [] | |
for i, task in enumerate(tasks): | |
if task in sub_tasks: | |
tasks_new_x_level1.extend([task] * len(selected_metrics)) | |
tasks_old_indices.append(i) | |
for sel_m in selected_metrics: | |
metric_data_list = metric_vars.get(sel_m) | |
if metric_data_list is not None and i < len(metric_data_list): | |
performance.append(metric_data_list[i]) | |
else: | |
performance.append(None) | |
print(f"Warning: Missing data for metric '{sel_m}' at index {i} in file {file_name} (Row {row_i})") | |
if not tasks_new_x_level1: continue | |
expected_len = len(tasks_new_x_level1) | |
if len(performance) != expected_len: | |
print(f"Warning: Length mismatch for {file_name} (Row {row_i}). X: {expected_len}, Y: {len(performance)}. Skipping trace.") | |
continue | |
x_level2 = selected_metric_labels * (len(tasks_new_x_level1) // len(selected_metric_labels)) | |
if len(tasks_new_x_level1) != len(x_level2): | |
print(f"Warning: X-axis level length mismatch for {file_name} (Row {row_i}). L1: {len(tasks_new_x_level1)}, L2: {len(x_level2)}. Skipping trace.") | |
continue | |
if idx == 0: | |
row_x_level1 = tasks_new_x_level1 | |
row_x_level2 = x_level2 | |
offset_group_name = f"group_{idx}" | |
fig.add_trace( | |
go.Bar( | |
x=[tasks_new_x_level1, x_level2], | |
y=performance, | |
name=file_name[:-15] if file_name.endswith('_statistics.txt') else file_name, | |
marker_color=colors[idx % len(colors)], | |
showlegend=(row_i == 1), | |
legendgroup=f"legend_{idx}", | |
offsetgroup=offset_group_name | |
), | |
row=row_i, | |
col=1 | |
) | |
if row_i not in added_traces_info: added_traces_info[row_i] = set() | |
added_traces_info[row_i].add(offset_group_name) | |
if row_x_level1 and row_x_level2: | |
fig.update_xaxes(type='multicategory', row=row_i, col=1) | |
# Multi-row layout updates | |
fig.update_layout( | |
barmode='group', | |
template="ggplot2", | |
autosize=False, | |
width=None, | |
height=total_fig_height, | |
title=" ".join(list(map(str.capitalize, main_metric.split("_")))), | |
margin=dict(l=margin_left, r=margin_right, t=margin_top, b=margin_bottom), | |
legend_title_text='Models' | |
) | |
for i in range(row_count): | |
is_last_row = (i == row_count - 1) | |
fig.update_xaxes( | |
showgrid=False, | |
tickangle=-45 if is_last_row else -30, | |
title_text='Task<br />Range' if is_last_row else '', | |
row=i+1, | |
col=1 | |
) | |
fig.update_yaxes( | |
title_text='Performance', | |
row=i+1, | |
col=1 | |
) | |
return fig | |
# ================== Styles ================== | |
SIDEBAR_STYLE = { | |
"position": "fixed", | |
"top": 0, | |
"left": 0, | |
"bottom": 0, | |
"width": "24rem", | |
"padding": "2rem 1rem", | |
"backgroundColor": "#FFFFFF", | |
"boxShadow": "2px 0px 5px rgba(0,0,0,0.1)", | |
"overflowY": "auto", | |
"transition": "all 0.3s", | |
"zIndex": 1000 # Sidebar itself | |
} | |
SIDEBAR_HIDDEN = { | |
**SIDEBAR_STYLE, | |
"left": "-24rem", # Move off-screen | |
"padding": "2rem 0", | |
} | |
CONTENT_STYLE = { | |
"marginLeft": "24rem", | |
"padding": "2rem 2rem", | |
"transition": "margin-left 0.3s", | |
"backgroundColor": "#F7F7F7", | |
"minHeight": "100vh", | |
} | |
CONTENT_STYLE_FULL = { | |
**CONTENT_STYLE, | |
"marginLeft": "0rem", | |
} | |
# --- NEW: Style for the fixed toggle button container --- | |
TOGGLE_BUTTON_STYLE = { | |
"position": "fixed", | |
"top": "10px", # Position from top | |
"left": "10px", # Position from left | |
"zIndex": 1001, # Ensure it's above the sidebar | |
"transition": "left 0.3s" # Optional: Animate button position slightly if needed | |
} | |
TOGGLE_BUTTON_STYLE_SHIFTED = { # Optional: Style when sidebar is open | |
**TOGGLE_BUTTON_STYLE, | |
"left": "calc(24rem + 10px)" # Position relative to open sidebar edge | |
# Or keep it fixed at "10px" - simpler | |
} | |
# ================== Dash 布局 (Dash Layout) ================== | |
app.layout = html.Div(style={"margin": "0", "padding": "0"}, children=[ | |
# --- NEW: Fixed Toggle Button Container --- | |
html.Div( | |
id="toggle-button-container", # Give it an ID if you want to style it dynamically | |
style=TOGGLE_BUTTON_STYLE, # Apply the fixed style | |
children=[ | |
html.Button( | |
"☰", # Use an icon/symbol for compactness | |
id="sidebar-toggle", | |
n_clicks=0, | |
style={ | |
"padding": "8px 12px", | |
"fontSize": "1.2em", | |
"backgroundColor": "#e9ecef", | |
"border": "1px solid #ccc", | |
"borderRadius": "5px", | |
"cursor": "pointer", | |
"boxShadow": "1px 1px 3px rgba(0,0,0,0.2)" | |
}, | |
title="Toggle Sidebar" # Tooltip | |
) | |
] | |
), | |
# --- Sidebar --- | |
html.Div( | |
id="sidebar", | |
style=SIDEBAR_STYLE, # Start open | |
children=[ | |
html.H3("Controls", style={"textAlign": "center", "marginBottom": "1.5rem", "marginTop": "2rem"}), # Add margin top to avoid button overlap | |
# --- REMOVED Button from here --- | |
# (Keep all the control sections: Metric, Files, Tasks, Ranges) | |
# Metric 选择单选框 | |
html.Div( | |
style={"marginBottom": "20px", "borderTop": "1px solid #eee", "paddingTop": "15px"}, | |
children=[ | |
html.Label("Select Metric:", style={"fontWeight": "bold", "display": "block", "marginBottom": "5px"}), | |
dcc.RadioItems( | |
id='metric-selector', | |
options=[ | |
{'label': 'Exact Match', 'value': 'exact_match'}, | |
{'label': 'Digit Match', 'value': 'digit_match'}, | |
{'label': 'Dlength', 'value': 'dlength'} | |
], | |
value='exact_match', | |
# inline=True, # Better stacked in sidebar | |
labelStyle={'display': 'block', 'marginBottom': '5px'}, | |
style={"marginBottom": "10px"} | |
), | |
] | |
), | |
# 文件选择栏 (Model Selection) | |
html.Div( | |
style={"marginBottom": "20px", "borderTop": "1px solid #eee", "paddingTop": "15px"}, | |
children=[ | |
html.Label("Select Models:", style={"fontWeight": "bold", "display": "block", "marginBottom": "5px"}), | |
dcc.Checklist( | |
id='file-selector-all-clear', | |
options=[ | |
{'label': 'Select All', 'value': 'all'}, | |
{'label': 'Clear', 'value': 'clear'}, | |
], | |
value=[], | |
inline=True, | |
style={"marginBottom": "5px"} | |
), | |
dcc.Checklist( | |
id='file-selector', | |
options=[{'label': file_name[:-15] if file_name.endswith('_statistics.txt') else file_name, 'value': file_name} for file_name in file_names], | |
value=[ # Default selection | |
'GPT-4o_statistics.txt', | |
'Llama-3.1-8B-ft_statistics.txt', | |
'Mixtral-8x7B_statistics.txt', | |
'Qwen2-72B_statistics.txt' | |
], | |
# inline=True, # Stacked looks better in narrow sidebar | |
labelStyle={'display': 'block', 'marginBottom': '3px'}, | |
style={"maxHeight": "200px", "overflowY": "auto", "border": "1px solid #ddd", "padding": "5px", "borderRadius": "4px"} # Scrollable list | |
), | |
] | |
), | |
# 任务选择 (Task Selection) | |
html.Div( | |
style={"marginBottom": "20px", "borderTop": "1px solid #eee", "paddingTop": "15px"}, | |
children=[ | |
html.Label("Select Tasks:", style={"fontWeight": "bold", "display": "block", "marginBottom": "10px"}), | |
html.H5("Integer Tasks", style={"fontWeight": "bold", "marginTop": "10px"}), | |
dcc.Checklist( id='int-task-selector-all-clear', options=[ {'label': 'All', 'value': 'all'}, {'label': 'Clear', 'value': 'clear'}], value=[], inline=True, style={"marginBottom": "5px"}), | |
dcc.Checklist( id='int-task-selector', options=[{'label': task, 'value': task + '<br />Integer'} for task in intTasks], value=['Add<br />Integer'], labelStyle={'display': 'block'}, style={"maxHeight": "150px", "overflowY": "auto", "border": "1px solid #ddd", "padding": "5px", "borderRadius": "4px", "marginBottom": "10px"}), | |
html.H5("Float Tasks", style={"fontWeight": "bold", "marginTop": "15px"}), | |
dcc.Checklist(id='float-task-selector-all-clear', options=[{'label': 'All', 'value': 'all'}, {'label': 'Clear', 'value': 'clear'}], value=[], inline=True, style={"marginBottom": "5px"}), | |
dcc.Checklist( id='float-task-selector', options=[{'label': task, 'value': task + '<br />Float'} for task in floatTasks], value=['Add<br />Float'], labelStyle={'display': 'block'}, style={"maxHeight": "150px", "overflowY": "auto", "border": "1px solid #ddd", "padding": "5px", "borderRadius": "4px", "marginBottom": "10px"}), | |
html.H5("Fraction Tasks", style={"fontWeight": "bold", "marginTop": "15px"}), | |
dcc.Checklist(id='fraction-task-selector-all-clear', options=[{'label': 'All', 'value': 'all'}, {'label': 'Clear', 'value': 'clear'}], value=[], inline=True, style={"marginBottom": "5px"}), | |
dcc.Checklist( id='fraction-task-selector', options=[{'label': task, 'value': task + '<br />Fraction'} for task in fractionTasks], value=['Add<br />Fraction'], labelStyle={'display': 'block'}, style={"maxHeight": "150px", "overflowY": "auto", "border": "1px solid #ddd", "padding": "5px", "borderRadius": "4px", "marginBottom": "10px"}), | |
html.H5("Scientific Tasks", style={"fontWeight": "bold", "marginTop": "15px"}), | |
dcc.Checklist(id='sci-task-selector-all-clear', options=[{'label': 'All', 'value': 'all'}, {'label': 'Clear', 'value': 'clear'}], value=[], inline=True, style={"marginBottom": "5px"}), | |
dcc.Checklist( id='sci-task-selector', options=[{'label': task, 'value': task + '<br />ScientificNotation'} for task in sciTasks], value=['Add<br />ScientificNotation'], labelStyle={'display': 'block'}, style={"maxHeight": "150px", "overflowY": "auto", "border": "1px solid #ddd", "padding": "5px", "borderRadius": "4px", "marginBottom": "10px"}), | |
] | |
), | |
# Range 选择 | |
html.Div( | |
style={"marginBottom": "20px", "borderTop": "1px solid #eee", "paddingTop": "15px"}, | |
children=[ | |
html.Label("Select Ranges:", style={"fontWeight": "bold", "display": "block", "marginBottom": "5px"}), | |
dcc.Checklist( | |
id='metrics-selector', # Keep ID, though it selects ranges now | |
options=[ | |
{'label': 'Short Range (S)', 'value': 'short_range'}, | |
{'label': 'Medium Range (M)', 'value': 'medium_range'}, | |
{'label': 'Long Range (L)', 'value': 'long_range'}, | |
{'label': 'Very Long Range (XL)', 'value': 'very_long_range'} | |
], | |
value=['short_range', 'medium_range', 'long_range', 'very_long_range'], # Default | |
# inline=True, # Stacked | |
labelStyle={'display': 'block', 'marginBottom': '5px'}, | |
style={"marginBottom": "10px"} | |
), | |
] | |
), | |
] | |
), # End Sidebar | |
# --- Main Content Area --- | |
html.Div( | |
id="content", | |
style=CONTENT_STYLE, # Start with margin for open sidebar | |
children=[ | |
# Add padding top to content to prevent overlap with fixed button | |
html.Div(style={"paddingTop": "50px"}, children=[ | |
html.H1( | |
"NUPA Performance", | |
style={"textAlign": "center", "marginBottom": "30px", "color": "#333"} | |
), | |
# Graph Area | |
html.Div( | |
style={ | |
"backgroundColor": "#FFFFFF", | |
"padding": "20px", | |
"borderRadius": "8px", | |
"boxShadow": "0 1px 4px rgba(0,0,0,0.1)", | |
"marginBottom": "20px", | |
}, | |
children=[ | |
dcc.Loading( | |
id="loading-graph", | |
type="circle", | |
children=dcc.Graph( | |
id='performance-plot', | |
style={"width": "100%", "height": "auto"} | |
) | |
) | |
] | |
) | |
]) # End padded content div | |
] | |
) # End Content Area | |
]) | |
# ================== Callbacks ================== | |
# --- Callback: Plotting (Keep existing) --- | |
def update_figure(main_metric, selected_files, selected_int_tasks, | |
selected_float_tasks, selected_fraction_tasks, | |
selected_sci_tasks, selected_ranges): # Renamed variable | |
selected_tasks = selected_int_tasks + selected_float_tasks + selected_fraction_tasks + selected_sci_tasks | |
r = (0, len(keys)) # Use all keys by default now, adjust if needed | |
# Pass selected_ranges instead of a fixed list | |
return plot(main_metric, selected_files, selected_ranges, selected_tasks, r) | |
# --- Callback: Sidebar Toggle (No change needed in logic) --- | |
def toggle_sidebar(n, current_sidebar_style, current_content_style): | |
if n is None or n == 0: | |
return no_update, no_update | |
# Check if sidebar is currently open based on content margin | |
if current_content_style.get("marginLeft") == "24rem": # If sidebar is currently open | |
new_sidebar_style = SIDEBAR_HIDDEN | |
new_content_style = CONTENT_STYLE_FULL | |
else: # If sidebar is currently hidden | |
new_sidebar_style = SIDEBAR_STYLE | |
new_content_style = CONTENT_STYLE | |
return new_sidebar_style, new_content_style | |
# --- Callbacks: "Select All / Clear" (Keep existing) --- | |
def make_task_selector_callback(output_id, output_all_clear_id, task_list, task_suffix): | |
def update_task_selector(all_clear_value): | |
if not all_clear_value: | |
raise exceptions.PreventUpdate | |
trigger = all_clear_value[-1] | |
if 'clear' == trigger: | |
return [], [] | |
elif 'all' == trigger: | |
all_values = [task + task_suffix for task in task_list] | |
return all_values, [] | |
raise exceptions.PreventUpdate | |
return update_task_selector | |
update_int_selector = make_task_selector_callback('int-task-selector', 'int-task-selector-all-clear', intTasks, '<br />Integer') | |
update_float_selector = make_task_selector_callback('float-task-selector', 'float-task-selector-all-clear', floatTasks, '<br />Float') | |
update_fraction_selector = make_task_selector_callback('fraction-task-selector', 'fraction-task-selector-all-clear', fractionTasks, '<br />Fraction') | |
update_sci_selector = make_task_selector_callback('sci-task-selector', 'sci-task-selector-all-clear', sciTasks, '<br />ScientificNotation') | |
# --- File Selector All/Clear --- | |
def update_file_selector(all_clear_value): | |
if not all_clear_value: | |
raise exceptions.PreventUpdate | |
trigger = all_clear_value[-1] | |
if 'clear' == trigger: | |
return [], [] | |
elif 'all' == trigger: | |
return file_names, [] | |
raise exceptions.PreventUpdate | |
# ================== Run App ================== | |
# (Keep existing run code) | |
if name == '__main__': | |
if not os.path.isdir(results_dir): | |
print(f"Error: The directory '{results_dir}' does not exist.") | |
print("Please create it and place the necessary statistics.txt files inside.") | |
elif not keys: | |
print(f"Warning: Could not load keys. Functionality might be limited. Check data files in {results_dir}") | |
else: | |
print(f"Looking for data files in: {os.path.abspath(results_dir)}") | |
# Only run if keys were loaded (or decide how to handle empty keys) | |
# if keys: | |
app.run(debug=True, host='0.0.0.0', port=7860) | |
# else: | |
# print("Exiting: Cannot run app without valid keys loaded.") |