diff --git a/main.py b/main.py index 67c5f3a..ca5d822 100644 --- a/main.py +++ b/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 - 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" diff --git a/src/components/bar_chart.py b/src/components/bar_chart.py index 06c376c..97781e5 100644 --- a/src/components/bar_chart.py +++ b/src/components/bar_chart.py @@ -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) diff --git a/src/components/category_dropdown.py b/src/components/category_dropdown.py index 2c1cc52..39dd85f 100644 --- a/src/components/category_dropdown.py +++ b/src/components/category_dropdown.py @@ -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=[ diff --git a/src/components/data_table.py b/src/components/data_table.py index d2065b5..dee8d84 100644 --- a/src/components/data_table.py +++ b/src/components/data_table.py @@ -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"), diff --git a/src/components/filter_card.py b/src/components/filter_card.py new file mode 100644 index 0000000..82c26b4 --- /dev/null +++ b/src/components/filter_card.py @@ -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") \ No newline at end of file diff --git a/src/components/ids.py b/src/components/ids.py index c9e2e0a..25e3225 100644 --- a/src/components/ids.py +++ b/src/components/ids.py @@ -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" diff --git a/src/components/layout.py b/src/components/layout.py index e6918b9..d3ab1f6 100644 --- a/src/components/layout.py +++ b/src/components/layout.py @@ -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=[ diff --git a/src/components/pie_chart.py b/src/components/pie_chart.py deleted file mode 100644 index 55f7f8d..0000000 --- a/src/components/pie_chart.py +++ /dev/null @@ -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}
$%{value:.2f}") - - return html.Div(dcc.Graph(figure=fig), id=ids.PIE_CHART) - - return html.Div(id=ids.PIE_CHART) diff --git a/src/components/source_table_tab.py b/src/components/source_table_tab.py index 96b0d78..1a4c4b2 100644 --- a/src/components/source_table_tab.py +++ b/src/components/source_table_tab.py @@ -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 diff --git a/src/components/week_dropdown.py b/src/components/week_dropdown.py deleted file mode 100644 index b405e33..0000000 --- a/src/components/week_dropdown.py +++ /dev/null @@ -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, - ), - ] - ) diff --git a/src/components/year_dropdown.py b/src/components/year_dropdown.py deleted file mode 100644 index 896cfe4..0000000 --- a/src/components/year_dropdown.py +++ /dev/null @@ -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, - ), - ] - )