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