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:
2025-09-18 05:20:13 +02:00
parent db30fa9b4e
commit 01da618733
11 changed files with 225 additions and 218 deletions

10
main.py
View File

@ -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
data = load_mtbf_data(config["DATA_PATH"])
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"

View File

@ -16,55 +16,55 @@ 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")
if filtered_data.shape[0] == 0:
return html.Div("No data selected.")
try:
if not all([year, week, span]):
return html.Div("No data selected.")
hits_col = MTBFSchema.U_HITS if u_hits else MTBFSchema.P_HITS
hits_type = "U-Hits" if u_hits else "P-Hits"
years = [year]
start_week = week - span + 1
weeks = list(range(start_week, week + 1))
# Count 'Y' values and create pareto
pareto_data = (
filtered_data[filtered_data[hits_col] == "Y"]
.groupby(MTBFSchema.AIR)
.size()
.reset_index(name="count")
)
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.")
if remove_single:
pareto_data = pareto_data[pareto_data["count"] > 1]
hits_col = MTBFSchema.U_HITS if u_hits else MTBFSchema.P_HITS
hits_type = "U-Hits" if u_hits else "P-Hits"
pareto_data = pareto_data.sort_values("count", ascending=False)
# Count 'Y' values and create pareto
pareto_data = (
filtered_data[filtered_data[hits_col] == "Y"]
.groupby([MTBFSchema.AIR, MTBFSchema.SYSTEM])
.size()
.reset_index(name="count")
)
fig = px.bar(
pareto_data,
x=MTBFSchema.AIR,
y="count",
title=f"{hits_type} per AIR (Pareto)",
labels={
MTBFSchema.AIR: "AIR",
"count": "Count",
},
)
return dcc.Graph(figure=fig)
if remove_single:
pareto_data = pareto_data[pareto_data["count"] > 1]
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),
]
)
pareto_data = pareto_data.sort_values("count", ascending=False)
fig = px.bar(
pareto_data,
x=MTBFSchema.AIR,
y="count",
color=MTBFSchema.SYSTEM,
title=f"{hits_type} per AIR (Pareto)",
labels={
MTBFSchema.AIR: "AIR",
"count": "Count",
},
)
return dcc.Graph(figure=fig)
except Exception as e:
return html.Div(f"An error occurred in bar_chart: {e}")
return html.Div(id=ids.BAR_CHART)

View File

@ -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]:
filtered_data = data.query("year in @years and week in @weeks")
return sorted(set(filtered_data[MTBFSchema.AIR].tolist()))
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=[

View File

@ -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,46 +15,68 @@ 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:
filtered_data = data.query(
"year in @years and week in @weeks"
)
if filtered_data.shape[0] == 0:
return html.Div("No data selected.")
try:
print(f'year: {year}, week: {week}, span: {span}, systems: {systems}')
if not all([year, week, span]):
return html.Div("No data selected.")
hits_col = MTBFSchema.U_HITS if u_hits else MTBFSchema.P_HITS
# Count 'Y' values
hits_data = filtered_data[filtered_data[hits_col] == 'Y']
table_data = hits_data.groupby(MTBFSchema.AIR).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()))
).reset_index()
if remove_single:
table_data = table_data[table_data['count'] > 1]
years = [year]
table_data = table_data.sort_values('count', ascending=False)
# Reorder columns
table_data = table_data[[MTBFSchema.AIR, 'air_issue_description', 'close_notes', 'count']]
start_week = week - span + 1
weeks = list(range(start_week, week + 1))
return dash_table.DataTable(
id=ids.MTBF_PAR_TABLE, # Using this ID for feedback callbacks
data=table_data.to_dict("records"),
columns=[{"name": i, "id": i} for i in table_data.columns],
page_size=10,
row_selectable='single',
selected_rows=[],
filter_action='native',
sort_action='native'
)
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.")
hits_col = MTBFSchema.U_HITS if u_hits else MTBFSchema.P_HITS
# Count 'Y' values
hits_data = filtered_data[filtered_data[hits_col] == 'Y']
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()))
).reset_index()
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, MTBFSchema.SYSTEM, 'air_issue_description', 'close_notes', 'count']]
return dash_table.DataTable(
id=ids.MTBF_PAR_TABLE, # Using this ID for feedback callbacks
data=table_data.to_dict("records"),
columns=[{"name": i, "id": i} for i in table_data.columns],
page_size=10,
row_selectable='single',
selected_rows=[],
filter_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"),

View 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")

View File

@ -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"

View File

@ -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=[

View File

@ -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)

View File

@ -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

View File

@ -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,
),
]
)

View File

@ -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,
),
]
)