feat: Improve dashboard functionality and stability
This commit introduces several improvements to the dashboard: - Refactored the component structure for consistency, defining callbacks within the `render` function. - Added robust error handling to data loading and callbacks to prevent crashes. - Implemented linking for SO and AIR columns in the source table. - Added and improved filtering and display options for tables. - Left-aligned columns in tables for better readability. - Cleaned up unused component files.
This commit is contained in:
8
main.py
8
main.py
@ -1,4 +1,3 @@
|
||||
# initial commit
|
||||
from dash import Dash
|
||||
import dash_bootstrap_components as dbc
|
||||
|
||||
@ -20,7 +19,14 @@ def main() -> None:
|
||||
print(os.getenv("MY_ENV_VAR"))
|
||||
print(config["Startup"])
|
||||
# load the data and create the data manager
|
||||
try:
|
||||
data = load_mtbf_data(config["DATA_PATH"])
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Data file not found at {config['DATA_PATH']}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"Error loading data: {e}")
|
||||
return
|
||||
|
||||
app = Dash(external_stylesheets=[BS])
|
||||
app.title = "Reliability Dashboard"
|
||||
|
@ -16,12 +16,23 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
Input(ids.WEEK_DROPDOWN, "value"),
|
||||
Input(ids.PU_HITS_SELECTOR, "value"),
|
||||
Input(ids.SINGLE_HITTER_FILTER, "value"),
|
||||
Input(ids.SYSTEM_FILTER, "value"),
|
||||
Input("span", "value"),
|
||||
],
|
||||
)
|
||||
def update_bar_chart(
|
||||
years: list[str], weeks: list[str], u_hits: bool, remove_single: bool
|
||||
year: int, week: int, u_hits: bool, remove_single: bool, systems: list[str], span: int
|
||||
) -> html.Div:
|
||||
filtered_data = data.query("year in @years and week in @weeks")
|
||||
try:
|
||||
if not all([year, week, span]):
|
||||
return html.Div("No data selected.")
|
||||
|
||||
years = [year]
|
||||
|
||||
start_week = week - span + 1
|
||||
weeks = list(range(start_week, week + 1))
|
||||
|
||||
filtered_data = data.query("year in @years and Week_Number in @weeks and System in @systems")
|
||||
if filtered_data.shape[0] == 0:
|
||||
return html.Div("No data selected.")
|
||||
|
||||
@ -31,7 +42,7 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
# Count 'Y' values and create pareto
|
||||
pareto_data = (
|
||||
filtered_data[filtered_data[hits_col] == "Y"]
|
||||
.groupby(MTBFSchema.AIR)
|
||||
.groupby([MTBFSchema.AIR, MTBFSchema.SYSTEM])
|
||||
.size()
|
||||
.reset_index(name="count")
|
||||
)
|
||||
@ -45,6 +56,7 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
pareto_data,
|
||||
x=MTBFSchema.AIR,
|
||||
y="count",
|
||||
color=MTBFSchema.SYSTEM,
|
||||
title=f"{hits_type} per AIR (Pareto)",
|
||||
labels={
|
||||
MTBFSchema.AIR: "AIR",
|
||||
@ -52,19 +64,7 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
},
|
||||
)
|
||||
return dcc.Graph(figure=fig)
|
||||
except Exception as e:
|
||||
return html.Div(f"An error occurred in bar_chart: {e}")
|
||||
|
||||
return html.Div(
|
||||
children=[
|
||||
dbc.Switch(
|
||||
id=ids.PU_HITS_SELECTOR,
|
||||
label="P-Hits / U-Hits",
|
||||
value=False,
|
||||
),
|
||||
dbc.Switch(
|
||||
id=ids.SINGLE_HITTER_FILTER,
|
||||
label="Remove single hitters",
|
||||
value=False,
|
||||
),
|
||||
html.Div(id=ids.BAR_CHART),
|
||||
]
|
||||
)
|
||||
return html.Div(id=ids.BAR_CHART)
|
||||
|
@ -26,8 +26,12 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
],
|
||||
)
|
||||
def select_all_airs(years: list[str], weeks: list[str], _: int) -> list[str]:
|
||||
try:
|
||||
filtered_data = data.query("year in @years and week in @weeks")
|
||||
return sorted(set(filtered_data[MTBFSchema.AIR].tolist()))
|
||||
except Exception as e:
|
||||
print(f"An error occurred in category_dropdown: {e}")
|
||||
return []
|
||||
|
||||
return html.Div(
|
||||
children=[
|
||||
|
@ -7,15 +7,6 @@ import dash_bootstrap_components as dbc
|
||||
from ..data.loader_gz import MTBFSchema
|
||||
from . import ids
|
||||
|
||||
import pandas as pd
|
||||
from dash import Dash, dcc, html, dash_table, Input, Output, State, callback_context
|
||||
from datetime import datetime
|
||||
import os
|
||||
import dash_bootstrap_components as dbc
|
||||
|
||||
from ..data.loader_gz import MTBFSchema
|
||||
from . import ids
|
||||
|
||||
def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
@app.callback(
|
||||
Output(ids.DATA_TABLE, "children"),
|
||||
@ -24,13 +15,25 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
Input(ids.WEEK_DROPDOWN, "value"),
|
||||
Input(ids.PU_HITS_SELECTOR, "value"),
|
||||
Input(ids.SINGLE_HITTER_FILTER, "value"),
|
||||
Input(ids.SYSTEM_FILTER, "value"),
|
||||
Input("span", "value"),
|
||||
],
|
||||
)
|
||||
def update_data_table(
|
||||
years: list[str], weeks: list[str], u_hits: bool, remove_single: bool
|
||||
year: int, week: int, u_hits: bool, remove_single: bool, systems: list[str], span: int
|
||||
) -> html.Div:
|
||||
try:
|
||||
print(f'year: {year}, week: {week}, span: {span}, systems: {systems}')
|
||||
if not all([year, week, span]):
|
||||
return html.Div("No data selected.")
|
||||
|
||||
years = [year]
|
||||
|
||||
start_week = week - span + 1
|
||||
weeks = list(range(start_week, week + 1))
|
||||
|
||||
filtered_data = data.query(
|
||||
"year in @years and week in @weeks"
|
||||
"year in @years and Week_Number in @weeks and System in @systems"
|
||||
)
|
||||
if filtered_data.shape[0] == 0:
|
||||
return html.Div("No data selected.")
|
||||
@ -40,7 +43,7 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
# Count 'Y' values
|
||||
hits_data = filtered_data[filtered_data[hits_col] == 'Y']
|
||||
|
||||
table_data = hits_data.groupby(MTBFSchema.AIR).agg(
|
||||
table_data = hits_data.groupby([MTBFSchema.AIR, MTBFSchema.SYSTEM]).agg(
|
||||
count=(hits_col, 'size'),
|
||||
air_issue_description=(MTBFSchema.AIR_ISSUE_DESCRIPTION, lambda x: ', '.join(x.dropna().astype(str).unique())),
|
||||
close_notes=(MTBFSchema.CLOSE_NOTES, lambda x: ', '.join(x.dropna().astype(str).unique()))
|
||||
@ -49,10 +52,12 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
if remove_single:
|
||||
table_data = table_data[table_data['count'] > 1]
|
||||
|
||||
table_data['air_issue_description'] = table_data['air_issue_description'].apply(lambda s: s[:80] + '...' if len(s) > 80 else s)
|
||||
|
||||
table_data = table_data.sort_values('count', ascending=False)
|
||||
|
||||
# Reorder columns
|
||||
table_data = table_data[[MTBFSchema.AIR, 'air_issue_description', 'close_notes', 'count']]
|
||||
table_data = table_data[[MTBFSchema.AIR, MTBFSchema.SYSTEM, 'air_issue_description', 'close_notes', 'count']]
|
||||
|
||||
return dash_table.DataTable(
|
||||
id=ids.MTBF_PAR_TABLE, # Using this ID for feedback callbacks
|
||||
@ -62,8 +67,16 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
row_selectable='single',
|
||||
selected_rows=[],
|
||||
filter_action='native',
|
||||
sort_action='native'
|
||||
sort_action='native',
|
||||
style_cell_conditional=[
|
||||
{
|
||||
'if': {'column_id': c},
|
||||
'textAlign': 'left'
|
||||
} for c in ['Close_notes', 'count']
|
||||
]
|
||||
)
|
||||
except Exception as e:
|
||||
return html.Div(f"An error occurred in data_table: {e}")
|
||||
|
||||
@app.callback(
|
||||
Output(ids.FEEDBACK_MODAL, "is_open"),
|
||||
|
70
src/components/filter_card.py
Normal file
70
src/components/filter_card.py
Normal file
@ -0,0 +1,70 @@
|
||||
import dash
|
||||
from dash import dcc, html, Input, Output, State, ctx, ALL
|
||||
import dash_bootstrap_components as dbc
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import plotly.graph_objects as go
|
||||
import plotly.express as px
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from . import ids
|
||||
|
||||
def render(app: dash.Dash, data: pd.DataFrame) -> dbc.Card:
|
||||
# These values are hardcoded for now, as in main13a.py
|
||||
# They should probably be derived from the data
|
||||
today = datetime.date.today()
|
||||
today_iso = today.isocalendar()
|
||||
default_year = today_iso.year
|
||||
default_week = today_iso.week
|
||||
|
||||
systems = data["System"].unique() if "System" in data.columns else ["GY86", "GY87", "GY88", "GY89"]
|
||||
|
||||
@app.callback(
|
||||
Output(ids.WEEK_DROPDOWN, "options"),
|
||||
Output(ids.WEEK_DROPDOWN, "value"),
|
||||
Input(ids.YEAR_DROPDOWN, "value"),
|
||||
)
|
||||
def update_week_options(year):
|
||||
if not year:
|
||||
return [], None
|
||||
|
||||
if year == today.year:
|
||||
end_week = default_week
|
||||
else:
|
||||
end_week = 52
|
||||
|
||||
week_options = [{"label": f"Week {w}", "value": w} for w in range(1, end_week + 1)]
|
||||
value = end_week
|
||||
return week_options, value
|
||||
|
||||
return dbc.Card([
|
||||
dbc.CardBody([
|
||||
dbc.Row([
|
||||
dbc.Col([dbc.Label("Span"),
|
||||
dcc.Dropdown(id="span",
|
||||
options=[{"label": "4 weeks", "value": 4},
|
||||
{"label": "13 weeks", "value": 13}],
|
||||
value=13, clearable=False)], width=2),
|
||||
dbc.Col([dbc.Label("Year"),
|
||||
dcc.Dropdown(id=ids.YEAR_DROPDOWN,
|
||||
options=[{"label": str(default_year), "value": default_year},
|
||||
{"label": str(default_year-1), "value": default_year-1}],
|
||||
value=default_year, clearable=False)], width=2),
|
||||
dbc.Col([dbc.Label("Last Week"), dcc.Dropdown(id=ids.WEEK_DROPDOWN, clearable=False, value=default_week)], width=2),
|
||||
dbc.Col([dbc.Label("Hit Type"),
|
||||
dbc.Switch(id=ids.PU_HITS_SELECTOR,
|
||||
label="Show u-type (off = p-hit, on = u-type)",
|
||||
value=False)], width=2),
|
||||
dbc.Col([dbc.Label("Filter"),
|
||||
dbc.Switch(id=ids.SINGLE_HITTER_FILTER,
|
||||
label="Remove single hitters",
|
||||
value=False)], width=2),
|
||||
], className="mb-3"),
|
||||
dbc.Row([
|
||||
dbc.Col([dbc.Label("Systems"),
|
||||
dbc.Checklist(id=ids.SYSTEM_FILTER,
|
||||
options=[{"label": system, "value": system} for system in systems],
|
||||
value=systems, inline=True, switch=True)], width=12)
|
||||
])
|
||||
])
|
||||
], className="mb-3")
|
@ -30,3 +30,4 @@ SOURCE_FEEDBACK_LABEL = "source-feedback-label"
|
||||
|
||||
PU_HITS_SELECTOR = "pu-hits-selector"
|
||||
SINGLE_HITTER_FILTER = "single-hitter-filter"
|
||||
SYSTEM_FILTER = "system-filter"
|
||||
|
@ -3,11 +3,10 @@ from dash import Dash, dcc, html
|
||||
from src.components import (
|
||||
bar_chart,
|
||||
data_table,
|
||||
year_dropdown,
|
||||
week_dropdown,
|
||||
feedback_tab,
|
||||
explanation_tab,
|
||||
source_table_tab,
|
||||
filter_card,
|
||||
)
|
||||
|
||||
def create_layout(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
@ -21,15 +20,9 @@ def create_layout(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
dcc.Tabs(id="tabs", value='tab-dashboard', children=[
|
||||
dcc.Tab(label='Dashboard', value='tab-dashboard', children=[
|
||||
html.Div(style=tab_content_style, children=[
|
||||
filter_card.render(app, data),
|
||||
bar_chart.render(app, data),
|
||||
data_table.render(app, data),
|
||||
html.Div(
|
||||
className="dropdown-container",
|
||||
children=[
|
||||
year_dropdown.render(app, data),
|
||||
week_dropdown.render(app, data),
|
||||
],
|
||||
),
|
||||
])
|
||||
]),
|
||||
dcc.Tab(label='Source Table', value='tab-source-table', children=[
|
||||
|
@ -1,41 +0,0 @@
|
||||
import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
from dash import Dash, dcc, html
|
||||
from dash.dependencies import Input, Output
|
||||
|
||||
from ..data.loader import DataSchema
|
||||
from . import ids
|
||||
|
||||
|
||||
def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
@app.callback(
|
||||
Output(ids.PIE_CHART, "children"),
|
||||
[
|
||||
Input(ids.YEAR_DROPDOWN, "value"),
|
||||
Input(ids.MONTH_DROPDOWN, "value"),
|
||||
Input(ids.CATEGORY_DROPDOWN, "value"),
|
||||
],
|
||||
)
|
||||
def update_pie_chart(
|
||||
years: list[str], months: list[str], categories: list[str]
|
||||
) -> html.Div:
|
||||
filtered_data = data.query(
|
||||
"year in @years and month in @months and category in @categories"
|
||||
)
|
||||
|
||||
if filtered_data.shape[0] == 0:
|
||||
return html.Div("No data selected.", id=ids.PIE_CHART)
|
||||
|
||||
pie = go.Pie(
|
||||
labels=filtered_data[DataSchema.CATEGORY].tolist(),
|
||||
values=filtered_data[DataSchema.AMOUNT].tolist(),
|
||||
hole=0.5,
|
||||
)
|
||||
|
||||
fig = go.Figure(data=[pie])
|
||||
fig.update_layout(margin={"t": 40, "b": 0, "l": 0, "r": 0})
|
||||
fig.update_traces(hovertemplate="%{label}<br>$%{value:.2f}<extra></extra>")
|
||||
|
||||
return html.Div(dcc.Graph(figure=fig), id=ids.PIE_CHART)
|
||||
|
||||
return html.Div(id=ids.PIE_CHART)
|
@ -30,7 +30,13 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
|
||||
if triggered_id == ids.SAVE_SOURCE_FEEDBACK_BUTTON and active_cell:
|
||||
selected_row_data = table_data[active_cell["row"]]
|
||||
so_number = selected_row_data[MTBFSchema.SO]
|
||||
so_markdown = selected_row_data[MTBFSchema.SO]
|
||||
|
||||
if so_markdown and so_markdown.startswith('[') and ']' in so_markdown:
|
||||
so_number = so_markdown[so_markdown.find('[')+1:so_markdown.find(']')]
|
||||
else:
|
||||
so_number = so_markdown
|
||||
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
if not comment:
|
||||
comment = ""
|
||||
@ -60,7 +66,11 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
)
|
||||
def update_feedback_so_label(active_cell, table_data):
|
||||
if active_cell:
|
||||
so_number = table_data[active_cell["row"]][MTBFSchema.SO]
|
||||
so_markdown = table_data[active_cell["row"]][MTBFSchema.SO]
|
||||
if so_markdown and so_markdown.startswith('[') and ']' in so_markdown:
|
||||
so_number = so_markdown[so_markdown.find('[')+1:so_markdown.find(']')]
|
||||
else:
|
||||
so_number = so_markdown
|
||||
return f"SO Number: {so_number}"
|
||||
return "SO Number: "
|
||||
|
||||
@ -80,15 +90,42 @@ def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
is_open=False,
|
||||
)
|
||||
|
||||
desired_columns = ['System', 'Customer', 'Root_Error', 'Year_Week_Number', 'SDT_TimeStamp', 'SO', 'AIR', 'Air_issue_description', 'Close_notes']
|
||||
existing_columns = [col for col in desired_columns if col in data.columns]
|
||||
|
||||
table_data = data[existing_columns].copy()
|
||||
|
||||
so_base_url = os.getenv('HTTP_SO_ADDR', '')
|
||||
air_base_url = os.getenv('HTTP_AIR_ADDR', '')
|
||||
|
||||
if so_base_url:
|
||||
table_data['SO'] = table_data['SO'].apply(lambda so: f'[{so}]({so_base_url}{so})' if so else '')
|
||||
if air_base_url:
|
||||
table_data['AIR'] = table_data['AIR'].apply(lambda air: f'[{air}]({air_base_url}{air})' if air else '')
|
||||
|
||||
columns_config = []
|
||||
for col in existing_columns:
|
||||
if col in ['SO', 'AIR']:
|
||||
columns_config.append({"name": col, "id": col, "presentation": "markdown"})
|
||||
else:
|
||||
columns_config.append({"name": col, "id": col})
|
||||
|
||||
|
||||
return html.Div([
|
||||
dash_table.DataTable(
|
||||
id=ids.SOURCE_TABLE,
|
||||
data=data.to_dict("records"),
|
||||
columns=[{"name": i, "id": i} for i in data.columns],
|
||||
data=table_data.to_dict("records"),
|
||||
columns=columns_config,
|
||||
page_size=15,
|
||||
sort_action="native",
|
||||
filter_action="native",
|
||||
cell_selectable=True,
|
||||
style_cell_conditional=[
|
||||
{
|
||||
'if': {'column_id': c},
|
||||
'textAlign': 'left'
|
||||
} for c in ['Air_issue_description', 'Close_notes']
|
||||
]
|
||||
),
|
||||
html.Div(id=ids.SOURCE_FEEDBACK_MESSAGE),
|
||||
modal
|
||||
|
@ -1,40 +0,0 @@
|
||||
import pandas as pd
|
||||
from dash import Dash, dcc, html
|
||||
from dash.dependencies import Input, Output
|
||||
|
||||
from ..data.loader import DataSchema
|
||||
from . import ids
|
||||
|
||||
def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
all_weeks: list[str] = data[DataSchema.WEEK].tolist()
|
||||
unique_weeks = sorted(list(set(all_weeks)), reverse=True)
|
||||
default_weeks = unique_weeks[:13]
|
||||
|
||||
@app.callback(
|
||||
Output(ids.WEEK_DROPDOWN, "value"),
|
||||
[
|
||||
Input(ids.YEAR_DROPDOWN, "value"),
|
||||
Input(ids.SELECT_ALL_WEEKS_BUTTON, "n_clicks"),
|
||||
],
|
||||
)
|
||||
def select_all_weeks(years: list[str], _: int) -> list[str]:
|
||||
filtered_data = data.query("year in @years")
|
||||
return sorted(set(filtered_data[DataSchema.WEEK].tolist()))
|
||||
|
||||
return html.Div(
|
||||
children=[
|
||||
html.H6("Week"),
|
||||
dcc.Dropdown(
|
||||
id=ids.WEEK_DROPDOWN,
|
||||
options=[{"label": f"{week[:4]}-{week[4:]}", "value": week} for week in unique_weeks],
|
||||
value=default_weeks,
|
||||
multi=True,
|
||||
),
|
||||
html.Button(
|
||||
className="dropdown-button",
|
||||
children=["Select All"],
|
||||
id=ids.SELECT_ALL_WEEKS_BUTTON,
|
||||
n_clicks=0,
|
||||
),
|
||||
]
|
||||
)
|
@ -1,36 +0,0 @@
|
||||
import pandas as pd
|
||||
from dash import Dash, dcc, html
|
||||
from dash.dependencies import Input, Output
|
||||
|
||||
from ..data.loader import DataSchema
|
||||
from . import ids
|
||||
|
||||
|
||||
def render(app: Dash, data: pd.DataFrame) -> html.Div:
|
||||
all_years: list[str] = data[DataSchema.YEAR].tolist()
|
||||
unique_years = sorted(set(all_years), key=int)
|
||||
|
||||
@app.callback(
|
||||
Output(ids.YEAR_DROPDOWN, "value"),
|
||||
Input(ids.SELECT_ALL_YEARS_BUTTON, "n_clicks"),
|
||||
)
|
||||
def select_all_years(_: int) -> list[str]:
|
||||
return unique_years
|
||||
|
||||
return html.Div(
|
||||
children=[
|
||||
html.H6("Year"),
|
||||
dcc.Dropdown(
|
||||
id=ids.YEAR_DROPDOWN,
|
||||
options=[{"label": year, "value": year} for year in unique_years],
|
||||
value=unique_years,
|
||||
multi=True,
|
||||
),
|
||||
html.Button(
|
||||
className="dropdown-button",
|
||||
children=["Select All"],
|
||||
id=ids.SELECT_ALL_YEARS_BUTTON,
|
||||
n_clicks=0,
|
||||
),
|
||||
]
|
||||
)
|
Reference in New Issue
Block a user