From 5db452f7d7e6fcc5f7a051ce0706ebabfc05a9a0 Mon Sep 17 00:00:00 2001 From: admin <51248046+danton267@users.noreply.github.com> Date: Thu, 19 May 2022 14:03:26 +0100 Subject: [PATCH 1/3] refactor v1 --- apps/dash-manufacture-spc-dashboard/app.py | 1210 +---------------- .../assets/{ => css}/base-styles.css | 0 .../assets/{ => css}/fonts.css | 0 .../assets/{ => css}/spc-custom-styles.css | 0 apps/dash-manufacture-spc-dashboard/index.py | 335 +++++ .../requirements.txt | 10 +- .../runtime.txt | 1 + .../utils/components.py | 465 +++++++ .../utils/graphs.py | 270 ++++ .../utils/helper_functions.py | 102 ++ 10 files changed, 1189 insertions(+), 1204 deletions(-) rename apps/dash-manufacture-spc-dashboard/assets/{ => css}/base-styles.css (100%) rename apps/dash-manufacture-spc-dashboard/assets/{ => css}/fonts.css (100%) rename apps/dash-manufacture-spc-dashboard/assets/{ => css}/spc-custom-styles.css (100%) create mode 100644 apps/dash-manufacture-spc-dashboard/index.py create mode 100644 apps/dash-manufacture-spc-dashboard/runtime.txt create mode 100644 apps/dash-manufacture-spc-dashboard/utils/components.py create mode 100644 apps/dash-manufacture-spc-dashboard/utils/graphs.py create mode 100644 apps/dash-manufacture-spc-dashboard/utils/helper_functions.py diff --git a/apps/dash-manufacture-spc-dashboard/app.py b/apps/dash-manufacture-spc-dashboard/app.py index 62fd54342..88e39a092 100644 --- a/apps/dash-manufacture-spc-dashboard/app.py +++ b/apps/dash-manufacture-spc-dashboard/app.py @@ -1,27 +1,21 @@ -import os -import pathlib - -import dash -import dash_core_components as dcc -import dash_html_components as html -from dash.dependencies import Input, Output, State -import dash_table -import plotly.graph_objs as go -import dash_daq as daq - +from dash import Dash import pandas as pd +from utils.helper_functions import init_df -app = dash.Dash( +app = Dash( __name__, meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}], + title="Manufacturing SPC Dashboard", + suppress_callback_exceptions=True, + update_title=None ) -app.title = "Manufacturing SPC Dashboard" server = app.server -app.config["suppress_callback_exceptions"] = True -APP_PATH = str(pathlib.Path(__file__).parent.resolve()) -df = pd.read_csv(os.path.join(APP_PATH, os.path.join("data", "spc_data.csv"))) +## Load data used across the app +df = pd.read_csv("data/spc_data.csv") +state_dict = init_df(df) +## Define constants used across the app params = list(df) max_length = len(df) @@ -31,1186 +25,4 @@ suffix_count = "_count" suffix_ooc_n = "_OOC_number" suffix_ooc_g = "_OOC_graph" -suffix_indicator = "_indicator" - - -def build_banner(): - return html.Div( - id="banner", - className="banner", - children=[ - html.Div( - id="banner-text", - children=[ - html.H5("Manufacturing SPC Dashboard"), - html.H6("Process Control and Exception Reporting"), - ], - ), - html.Div( - id="banner-logo", - children=[ - html.A( - html.Button(children="ENTERPRISE DEMO"), - href="https://plotly.com/get-demo/", - ), - html.Button( - id="learn-more-button", children="LEARN MORE", n_clicks=0 - ), - html.A( - html.Img(id="logo", src=app.get_asset_url("dash-logo-new.png")), - href="https://plotly.com/dash/", - ), - ], - ), - ], - ) - - -def build_tabs(): - return html.Div( - id="tabs", - className="tabs", - children=[ - dcc.Tabs( - id="app-tabs", - value="tab2", - className="custom-tabs", - children=[ - dcc.Tab( - id="Specs-tab", - label="Specification Settings", - value="tab1", - className="custom-tab", - selected_className="custom-tab--selected", - ), - dcc.Tab( - id="Control-chart-tab", - label="Control Charts Dashboard", - value="tab2", - className="custom-tab", - selected_className="custom-tab--selected", - ), - ], - ) - ], - ) - - -def init_df(): - ret = {} - for col in list(df[1:]): - data = df[col] - stats = data.describe() - - std = stats["std"].tolist() - ucl = (stats["mean"] + 3 * stats["std"]).tolist() - lcl = (stats["mean"] - 3 * stats["std"]).tolist() - usl = (stats["mean"] + stats["std"]).tolist() - lsl = (stats["mean"] - stats["std"]).tolist() - - ret.update( - { - col: { - "count": stats["count"].tolist(), - "data": data, - "mean": stats["mean"].tolist(), - "std": std, - "ucl": round(ucl, 3), - "lcl": round(lcl, 3), - "usl": round(usl, 3), - "lsl": round(lsl, 3), - "min": stats["min"].tolist(), - "max": stats["max"].tolist(), - "ooc": populate_ooc(data, ucl, lcl), - } - } - ) - - return ret - - -def populate_ooc(data, ucl, lcl): - ooc_count = 0 - ret = [] - for i in range(len(data)): - if data[i] >= ucl or data[i] <= lcl: - ooc_count += 1 - ret.append(ooc_count / (i + 1)) - else: - ret.append(ooc_count / (i + 1)) - return ret - - -state_dict = init_df() - - -def init_value_setter_store(): - # Initialize store data - state_dict = init_df() - return state_dict - - -def build_tab_1(): - return [ - # Manually select metrics - html.Div( - id="set-specs-intro-container", - # className='twelve columns', - children=html.P( - "Use historical control limits to establish a benchmark, or set new values." - ), - ), - html.Div( - id="settings-menu", - children=[ - html.Div( - id="metric-select-menu", - # className='five columns', - children=[ - html.Label(id="metric-select-title", children="Select Metrics"), - html.Br(), - dcc.Dropdown( - id="metric-select-dropdown", - options=list( - {"label": param, "value": param} for param in params[1:] - ), - value=params[1], - ), - ], - ), - html.Div( - id="value-setter-menu", - # className='six columns', - children=[ - html.Div(id="value-setter-panel"), - html.Br(), - html.Div( - id="button-div", - children=[ - html.Button("Update", id="value-setter-set-btn"), - html.Button( - "View current setup", - id="value-setter-view-btn", - n_clicks=0, - ), - ], - ), - html.Div( - id="value-setter-view-output", className="output-datatable" - ), - ], - ), - ], - ), - ] - - -ud_usl_input = daq.NumericInput( - id="ud_usl_input", className="setting-input", size=200, max=9999999 -) -ud_lsl_input = daq.NumericInput( - id="ud_lsl_input", className="setting-input", size=200, max=9999999 -) -ud_ucl_input = daq.NumericInput( - id="ud_ucl_input", className="setting-input", size=200, max=9999999 -) -ud_lcl_input = daq.NumericInput( - id="ud_lcl_input", className="setting-input", size=200, max=9999999 -) - - -def build_value_setter_line(line_num, label, value, col3): - return html.Div( - id=line_num, - children=[ - html.Label(label, className="four columns"), - html.Label(value, className="four columns"), - html.Div(col3, className="four columns"), - ], - className="row", - ) - - -def generate_modal(): - return html.Div( - id="markdown", - className="modal", - children=( - html.Div( - id="markdown-container", - className="markdown-container", - children=[ - html.Div( - className="close-container", - children=html.Button( - "Close", - id="markdown_close", - n_clicks=0, - className="closeButton", - ), - ), - html.Div( - className="markdown-text", - children=dcc.Markdown( - children=( - """ - ###### What is this mock app about? - - This is a dashboard for monitoring real-time process quality along manufacture production line. - - ###### What does this app shows - - Click on buttons in `Parameter` column to visualize details of measurement trendlines on the bottom panel. - - The sparkline on top panel and control chart on bottom panel show Shewhart process monitor using mock data. - The trend is updated every other second to simulate real-time measurements. Data falling outside of six-sigma control limit are signals indicating 'Out of Control(OOC)', and will - trigger alerts instantly for a detailed checkup. - - Operators may stop measurement by clicking on `Stop` button, and edit specification parameters by clicking specification tab. - - ###### Source Code - - You can find the source code of this app on our [Github repository](https://github.com/plotly/dash-sample-apps/tree/main/apps/dash-manufacture-spc-dashboard). - - """ - ) - ), - ), - ], - ) - ), - ) - - -def build_quick_stats_panel(): - return html.Div( - id="quick-stats", - className="row", - children=[ - html.Div( - id="card-1", - children=[ - html.P("Operator ID"), - daq.LEDDisplay( - id="operator-led", - value="1704", - color="#92e0d3", - backgroundColor="#1e2130", - size=50, - ), - ], - ), - html.Div( - id="card-2", - children=[ - html.P("Time to completion"), - daq.Gauge( - id="progress-gauge", - max=max_length * 2, - min=0, - showCurrentValue=True, # default size 200 pixel - ), - ], - ), - html.Div( - id="utility-card", - children=[daq.StopButton(id="stop-button", size=160, n_clicks=0)], - ), - ], - ) - - -def generate_section_banner(title): - return html.Div(className="section-banner", children=title) - - -def build_top_panel(stopped_interval): - return html.Div( - id="top-section-container", - className="row", - children=[ - # Metrics summary - html.Div( - id="metric-summary-session", - className="eight columns", - children=[ - generate_section_banner("Process Control Metrics Summary"), - html.Div( - id="metric-div", - children=[ - generate_metric_list_header(), - html.Div( - id="metric-rows", - children=[ - generate_metric_row_helper(stopped_interval, 1), - generate_metric_row_helper(stopped_interval, 2), - generate_metric_row_helper(stopped_interval, 3), - generate_metric_row_helper(stopped_interval, 4), - generate_metric_row_helper(stopped_interval, 5), - generate_metric_row_helper(stopped_interval, 6), - generate_metric_row_helper(stopped_interval, 7), - ], - ), - ], - ), - ], - ), - # Piechart - html.Div( - id="ooc-piechart-outer", - className="four columns", - children=[ - generate_section_banner("% OOC per Parameter"), - generate_piechart(), - ], - ), - ], - ) - - -def generate_piechart(): - return dcc.Graph( - id="piechart", - figure={ - "data": [ - { - "labels": [], - "values": [], - "type": "pie", - "marker": {"line": {"color": "white", "width": 1}}, - "hoverinfo": "label", - "textinfo": "label", - } - ], - "layout": { - "margin": dict(l=20, r=20, t=20, b=20), - "showlegend": True, - "paper_bgcolor": "rgba(0,0,0,0)", - "plot_bgcolor": "rgba(0,0,0,0)", - "font": {"color": "white"}, - "autosize": True, - }, - }, - ) - - -# Build header -def generate_metric_list_header(): - return generate_metric_row( - "metric_header", - {"height": "3rem", "margin": "1rem 0", "textAlign": "center"}, - {"id": "m_header_1", "children": html.Div("Parameter")}, - {"id": "m_header_2", "children": html.Div("Count")}, - {"id": "m_header_3", "children": html.Div("Sparkline")}, - {"id": "m_header_4", "children": html.Div("OOC%")}, - {"id": "m_header_5", "children": html.Div("%OOC")}, - {"id": "m_header_6", "children": "Pass/Fail"}, - ) - - -def generate_metric_row_helper(stopped_interval, index): - item = params[index] - - div_id = item + suffix_row - button_id = item + suffix_button_id - sparkline_graph_id = item + suffix_sparkline_graph - count_id = item + suffix_count - ooc_percentage_id = item + suffix_ooc_n - ooc_graph_id = item + suffix_ooc_g - indicator_id = item + suffix_indicator - - return generate_metric_row( - div_id, - None, - { - "id": item, - "className": "metric-row-button-text", - "children": html.Button( - id=button_id, - className="metric-row-button", - children=item, - title="Click to visualize live SPC chart", - n_clicks=0, - ), - }, - {"id": count_id, "children": "0"}, - { - "id": item + "_sparkline", - "children": dcc.Graph( - id=sparkline_graph_id, - style={"width": "100%", "height": "95%"}, - config={ - "staticPlot": False, - "editable": False, - "displayModeBar": False, - }, - figure=go.Figure( - { - "data": [ - { - "x": state_dict["Batch"]["data"].tolist()[ - :stopped_interval - ], - "y": state_dict[item]["data"][:stopped_interval], - "mode": "lines+markers", - "name": item, - "line": {"color": "#f4d44d"}, - } - ], - "layout": { - "uirevision": True, - "margin": dict(l=0, r=0, t=4, b=4, pad=0), - "xaxis": dict( - showline=False, - showgrid=False, - zeroline=False, - showticklabels=False, - ), - "yaxis": dict( - showline=False, - showgrid=False, - zeroline=False, - showticklabels=False, - ), - "paper_bgcolor": "rgba(0,0,0,0)", - "plot_bgcolor": "rgba(0,0,0,0)", - }, - } - ), - ), - }, - {"id": ooc_percentage_id, "children": "0.00%"}, - { - "id": ooc_graph_id + "_container", - "children": daq.GraduatedBar( - id=ooc_graph_id, - color={ - "ranges": { - "#92e0d3": [0, 3], - "#f4d44d ": [3, 7], - "#f45060": [7, 15], - } - }, - showCurrentValue=False, - max=15, - value=0, - ), - }, - { - "id": item + "_pf", - "children": daq.Indicator( - id=indicator_id, value=True, color="#91dfd2", size=12 - ), - }, - ) - - -def generate_metric_row(id, style, col1, col2, col3, col4, col5, col6): - if style is None: - style = {"height": "8rem", "width": "100%"} - - return html.Div( - id=id, - className="row metric-row", - style=style, - children=[ - html.Div( - id=col1["id"], - className="one column", - style={"margin-right": "2.5rem", "minWidth": "50px"}, - children=col1["children"], - ), - html.Div( - id=col2["id"], - style={"textAlign": "center"}, - className="one column", - children=col2["children"], - ), - html.Div( - id=col3["id"], - style={"height": "100%"}, - className="four columns", - children=col3["children"], - ), - html.Div( - id=col4["id"], - style={}, - className="one column", - children=col4["children"], - ), - html.Div( - id=col5["id"], - style={"height": "100%", "margin-top": "5rem"}, - className="three columns", - children=col5["children"], - ), - html.Div( - id=col6["id"], - style={"display": "flex", "justifyContent": "center"}, - className="one column", - children=col6["children"], - ), - ], - ) - - -def build_chart_panel(): - return html.Div( - id="control-chart-container", - className="twelve columns", - children=[ - generate_section_banner("Live SPC Chart"), - dcc.Graph( - id="control-chart-live", - figure=go.Figure( - { - "data": [ - { - "x": [], - "y": [], - "mode": "lines+markers", - "name": params[1], - } - ], - "layout": { - "paper_bgcolor": "rgba(0,0,0,0)", - "plot_bgcolor": "rgba(0,0,0,0)", - "xaxis": dict( - showline=False, showgrid=False, zeroline=False - ), - "yaxis": dict( - showgrid=False, showline=False, zeroline=False - ), - "autosize": True, - }, - } - ), - ), - ], - ) - - -def generate_graph(interval, specs_dict, col): - stats = state_dict[col] - col_data = stats["data"] - mean = stats["mean"] - ucl = specs_dict[col]["ucl"] - lcl = specs_dict[col]["lcl"] - usl = specs_dict[col]["usl"] - lsl = specs_dict[col]["lsl"] - - x_array = state_dict["Batch"]["data"].tolist() - y_array = col_data.tolist() - - total_count = 0 - - if interval > max_length: - total_count = max_length - 1 - elif interval > 0: - total_count = interval - - ooc_trace = { - "x": [], - "y": [], - "name": "Out of Control", - "mode": "markers", - "marker": dict(color="rgba(210, 77, 87, 0.7)", symbol="square", size=11), - } - - for index, data in enumerate(y_array[:total_count]): - if data >= ucl or data <= lcl: - ooc_trace["x"].append(index + 1) - ooc_trace["y"].append(data) - - histo_trace = { - "x": x_array[:total_count], - "y": y_array[:total_count], - "type": "histogram", - "orientation": "h", - "name": "Distribution", - "xaxis": "x2", - "yaxis": "y2", - "marker": {"color": "#f4d44d"}, - } - - fig = { - "data": [ - { - "x": x_array[:total_count], - "y": y_array[:total_count], - "mode": "lines+markers", - "name": col, - "line": {"color": "#f4d44d"}, - }, - ooc_trace, - histo_trace, - ] - } - - len_figure = len(fig["data"][0]["x"]) - - fig["layout"] = dict( - margin=dict(t=40), - hovermode="closest", - uirevision=col, - paper_bgcolor="rgba(0,0,0,0)", - plot_bgcolor="rgba(0,0,0,0)", - legend={"font": {"color": "darkgray"}, "orientation": "h", "x": 0, "y": 1.1}, - font={"color": "darkgray"}, - showlegend=True, - xaxis={ - "zeroline": False, - "showgrid": False, - "title": "Batch Number", - "showline": False, - "domain": [0, 0.8], - "titlefont": {"color": "darkgray"}, - }, - yaxis={ - "title": col, - "showgrid": False, - "showline": False, - "zeroline": False, - "autorange": True, - "titlefont": {"color": "darkgray"}, - }, - annotations=[ - { - "x": 0.75, - "y": lcl, - "xref": "paper", - "yref": "y", - "text": "LCL:" + str(round(lcl, 3)), - "showarrow": False, - "font": {"color": "white"}, - }, - { - "x": 0.75, - "y": ucl, - "xref": "paper", - "yref": "y", - "text": "UCL: " + str(round(ucl, 3)), - "showarrow": False, - "font": {"color": "white"}, - }, - { - "x": 0.75, - "y": usl, - "xref": "paper", - "yref": "y", - "text": "USL: " + str(round(usl, 3)), - "showarrow": False, - "font": {"color": "white"}, - }, - { - "x": 0.75, - "y": lsl, - "xref": "paper", - "yref": "y", - "text": "LSL: " + str(round(lsl, 3)), - "showarrow": False, - "font": {"color": "white"}, - }, - { - "x": 0.75, - "y": mean, - "xref": "paper", - "yref": "y", - "text": "Targeted mean: " + str(round(mean, 3)), - "showarrow": False, - "font": {"color": "white"}, - }, - ], - shapes=[ - { - "type": "line", - "xref": "x", - "yref": "y", - "x0": 1, - "y0": usl, - "x1": len_figure + 1, - "y1": usl, - "line": {"color": "#91dfd2", "width": 1, "dash": "dot"}, - }, - { - "type": "line", - "xref": "x", - "yref": "y", - "x0": 1, - "y0": lsl, - "x1": len_figure + 1, - "y1": lsl, - "line": {"color": "#91dfd2", "width": 1, "dash": "dot"}, - }, - { - "type": "line", - "xref": "x", - "yref": "y", - "x0": 1, - "y0": ucl, - "x1": len_figure + 1, - "y1": ucl, - "line": {"color": "rgb(255,127,80)", "width": 1, "dash": "dot"}, - }, - { - "type": "line", - "xref": "x", - "yref": "y", - "x0": 1, - "y0": mean, - "x1": len_figure + 1, - "y1": mean, - "line": {"color": "rgb(255,127,80)", "width": 2}, - }, - { - "type": "line", - "xref": "x", - "yref": "y", - "x0": 1, - "y0": lcl, - "x1": len_figure + 1, - "y1": lcl, - "line": {"color": "rgb(255,127,80)", "width": 1, "dash": "dot"}, - }, - ], - xaxis2={ - "title": "Count", - "domain": [0.8, 1], # 70 to 100 % of width - "titlefont": {"color": "darkgray"}, - "showgrid": False, - }, - yaxis2={ - "anchor": "free", - "overlaying": "y", - "side": "right", - "showticklabels": False, - "titlefont": {"color": "darkgray"}, - }, - ) - - return fig - - -def update_sparkline(interval, param): - x_array = state_dict["Batch"]["data"].tolist() - y_array = state_dict[param]["data"].tolist() - - if interval == 0: - x_new = y_new = None - - else: - if interval >= max_length: - total_count = max_length - else: - total_count = interval - x_new = x_array[:total_count][-1] - y_new = y_array[:total_count][-1] - - return dict(x=[[x_new]], y=[[y_new]]), [0] - - -def update_count(interval, col, data): - if interval == 0: - return "0", "0.00%", 0.00001, "#92e0d3" - - if interval > 0: - - if interval >= max_length: - total_count = max_length - 1 - else: - total_count = interval - 1 - - ooc_percentage_f = data[col]["ooc"][total_count] * 100 - ooc_percentage_str = "%.2f" % ooc_percentage_f + "%" - - # Set maximum ooc to 15 for better grad bar display - if ooc_percentage_f > 15: - ooc_percentage_f = 15 - - if ooc_percentage_f == 0.0: - ooc_grad_val = 0.00001 - else: - ooc_grad_val = float(ooc_percentage_f) - - # Set indicator theme according to threshold 5% - if 0 <= ooc_grad_val <= 5: - color = "#92e0d3" - elif 5 < ooc_grad_val < 7: - color = "#f4d44d" - else: - color = "#FF0000" - - return str(total_count + 1), ooc_percentage_str, ooc_grad_val, color - - -app.layout = html.Div( - id="big-app-container", - children=[ - build_banner(), - dcc.Interval( - id="interval-component", - interval=2 * 1000, # in milliseconds - n_intervals=50, # start at batch 50 - disabled=True, - ), - html.Div( - id="app-container", - children=[ - build_tabs(), - # Main app - html.Div(id="app-content"), - ], - ), - dcc.Store(id="value-setter-store", data=init_value_setter_store()), - dcc.Store(id="n-interval-stage", data=50), - generate_modal(), - ], -) - - -@app.callback( - [Output("app-content", "children"), Output("interval-component", "n_intervals")], - [Input("app-tabs", "value")], - [State("n-interval-stage", "data")], -) -def render_tab_content(tab_switch, stopped_interval): - if tab_switch == "tab1": - return build_tab_1(), stopped_interval - return ( - html.Div( - id="status-container", - children=[ - build_quick_stats_panel(), - html.Div( - id="graphs-container", - children=[build_top_panel(stopped_interval), build_chart_panel()], - ), - ], - ), - stopped_interval, - ) - - -# Update interval -@app.callback( - Output("n-interval-stage", "data"), - [Input("app-tabs", "value")], - [ - State("interval-component", "n_intervals"), - State("interval-component", "disabled"), - State("n-interval-stage", "data"), - ], -) -def update_interval_state(tab_switch, cur_interval, disabled, cur_stage): - if disabled: - return cur_interval - - if tab_switch == "tab1": - return cur_interval - return cur_stage - - -# Callbacks for stopping interval update -@app.callback( - [Output("interval-component", "disabled"), Output("stop-button", "buttonText")], - [Input("stop-button", "n_clicks")], - [State("interval-component", "disabled")], -) -def stop_production(n_clicks, current): - if n_clicks == 0: - return True, "start" - return not current, "stop" if current else "start" - - -# ======= Callbacks for modal popup ======= -@app.callback( - Output("markdown", "style"), - [Input("learn-more-button", "n_clicks"), Input("markdown_close", "n_clicks")], -) -def update_click_output(button_click, close_click): - ctx = dash.callback_context - - if ctx.triggered: - prop_id = ctx.triggered[0]["prop_id"].split(".")[0] - if prop_id == "learn-more-button": - return {"display": "block"} - - return {"display": "none"} - - -# ======= update progress gauge ========= -@app.callback( - output=Output("progress-gauge", "value"), - inputs=[Input("interval-component", "n_intervals")], -) -def update_gauge(interval): - if interval < max_length: - total_count = interval - else: - total_count = max_length - - return int(total_count) - - -# ===== Callbacks to update values based on store data and dropdown selection ===== -@app.callback( - output=[ - Output("value-setter-panel", "children"), - Output("ud_usl_input", "value"), - Output("ud_lsl_input", "value"), - Output("ud_ucl_input", "value"), - Output("ud_lcl_input", "value"), - ], - inputs=[Input("metric-select-dropdown", "value")], - state=[State("value-setter-store", "data")], -) -def build_value_setter_panel(dd_select, state_value): - return ( - [ - build_value_setter_line( - "value-setter-panel-header", - "Specs", - "Historical Value", - "Set new value", - ), - build_value_setter_line( - "value-setter-panel-usl", - "Upper Specification limit", - state_dict[dd_select]["usl"], - ud_usl_input, - ), - build_value_setter_line( - "value-setter-panel-lsl", - "Lower Specification limit", - state_dict[dd_select]["lsl"], - ud_lsl_input, - ), - build_value_setter_line( - "value-setter-panel-ucl", - "Upper Control limit", - state_dict[dd_select]["ucl"], - ud_ucl_input, - ), - build_value_setter_line( - "value-setter-panel-lcl", - "Lower Control limit", - state_dict[dd_select]["lcl"], - ud_lcl_input, - ), - ], - state_value[dd_select]["usl"], - state_value[dd_select]["lsl"], - state_value[dd_select]["ucl"], - state_value[dd_select]["lcl"], - ) - - -# ====== Callbacks to update stored data via click ===== -@app.callback( - output=Output("value-setter-store", "data"), - inputs=[Input("value-setter-set-btn", "n_clicks")], - state=[ - State("metric-select-dropdown", "value"), - State("value-setter-store", "data"), - State("ud_usl_input", "value"), - State("ud_lsl_input", "value"), - State("ud_ucl_input", "value"), - State("ud_lcl_input", "value"), - ], -) -def set_value_setter_store(set_btn, param, data, usl, lsl, ucl, lcl): - if set_btn is None: - return data - else: - data[param]["usl"] = usl - data[param]["lsl"] = lsl - data[param]["ucl"] = ucl - data[param]["lcl"] = lcl - - # Recalculate ooc in case of param updates - data[param]["ooc"] = populate_ooc(df[param], ucl, lcl) - return data - - -@app.callback( - output=Output("value-setter-view-output", "children"), - inputs=[ - Input("value-setter-view-btn", "n_clicks"), - Input("metric-select-dropdown", "value"), - Input("value-setter-store", "data"), - ], -) -def show_current_specs(n_clicks, dd_select, store_data): - if n_clicks > 0: - curr_col_data = store_data[dd_select] - new_df_dict = { - "Specs": [ - "Upper Specification Limit", - "Lower Specification Limit", - "Upper Control Limit", - "Lower Control Limit", - ], - "Current Setup": [ - curr_col_data["usl"], - curr_col_data["lsl"], - curr_col_data["ucl"], - curr_col_data["lcl"], - ], - } - new_df = pd.DataFrame.from_dict(new_df_dict) - return dash_table.DataTable( - style_header={"fontWeight": "bold", "color": "inherit"}, - style_as_list_view=True, - fill_width=True, - style_cell_conditional=[ - {"if": {"column_id": "Specs"}, "textAlign": "left"} - ], - style_cell={ - "backgroundColor": "#1e2130", - "fontFamily": "Open Sans", - "padding": "0 2rem", - "color": "darkgray", - "border": "none", - }, - css=[ - {"selector": "tr:hover td", "rule": "color: #91dfd2 !important;"}, - {"selector": "td", "rule": "border: none !important;"}, - { - "selector": ".dash-cell.focused", - "rule": "background-color: #1e2130 !important;", - }, - {"selector": "table", "rule": "--accent: #1e2130;"}, - {"selector": "tr", "rule": "background-color: transparent"}, - ], - data=new_df.to_dict("rows"), - columns=[{"id": c, "name": c} for c in ["Specs", "Current Setup"]], - ) - - -# decorator for list of output -def create_callback(param): - def callback(interval, stored_data): - count, ooc_n, ooc_g_value, indicator = update_count( - interval, param, stored_data - ) - spark_line_data = update_sparkline(interval, param) - return count, spark_line_data, ooc_n, ooc_g_value, indicator - - return callback - - -for param in params[1:]: - update_param_row_function = create_callback(param) - app.callback( - output=[ - Output(param + suffix_count, "children"), - Output(param + suffix_sparkline_graph, "extendData"), - Output(param + suffix_ooc_n, "children"), - Output(param + suffix_ooc_g, "value"), - Output(param + suffix_indicator, "color"), - ], - inputs=[Input("interval-component", "n_intervals")], - state=[State("value-setter-store", "data")], - )(update_param_row_function) - - -# ======= button to choose/update figure based on click ============ -@app.callback( - output=Output("control-chart-live", "figure"), - inputs=[ - Input("interval-component", "n_intervals"), - Input(params[1] + suffix_button_id, "n_clicks"), - Input(params[2] + suffix_button_id, "n_clicks"), - Input(params[3] + suffix_button_id, "n_clicks"), - Input(params[4] + suffix_button_id, "n_clicks"), - Input(params[5] + suffix_button_id, "n_clicks"), - Input(params[6] + suffix_button_id, "n_clicks"), - Input(params[7] + suffix_button_id, "n_clicks"), - ], - state=[State("value-setter-store", "data"), State("control-chart-live", "figure")], -) -def update_control_chart(interval, n1, n2, n3, n4, n5, n6, n7, data, cur_fig): - # Find which one has been triggered - ctx = dash.callback_context - - if not ctx.triggered: - return generate_graph(interval, data, params[1]) - - if ctx.triggered: - # Get most recently triggered id and prop_type - splitted = ctx.triggered[0]["prop_id"].split(".") - prop_id = splitted[0] - prop_type = splitted[1] - - if prop_type == "n_clicks": - curr_id = cur_fig["data"][0]["name"] - prop_id = prop_id[:-7] - if curr_id == prop_id: - return generate_graph(interval, data, curr_id) - else: - return generate_graph(interval, data, prop_id) - - if prop_type == "n_intervals" and cur_fig is not None: - curr_id = cur_fig["data"][0]["name"] - return generate_graph(interval, data, curr_id) - - -# Update piechart -@app.callback( - output=Output("piechart", "figure"), - inputs=[Input("interval-component", "n_intervals")], - state=[State("value-setter-store", "data")], -) -def update_piechart(interval, stored_data): - if interval == 0: - return { - "data": [], - "layout": { - "font": {"color": "white"}, - "paper_bgcolor": "rgba(0,0,0,0)", - "plot_bgcolor": "rgba(0,0,0,0)", - }, - } - - if interval >= max_length: - total_count = max_length - 1 - else: - total_count = interval - 1 - - values = [] - colors = [] - for param in params[1:]: - ooc_param = (stored_data[param]["ooc"][total_count] * 100) + 1 - values.append(ooc_param) - if ooc_param > 6: - colors.append("#f45060") - else: - colors.append("#91dfd2") - - new_figure = { - "data": [ - { - "labels": params[1:], - "values": values, - "type": "pie", - "marker": {"colors": colors, "line": dict(color="white", width=2)}, - "hoverinfo": "label", - "textinfo": "label", - } - ], - "layout": { - "margin": dict(t=20, b=50), - "uirevision": True, - "font": {"color": "white"}, - "showlegend": False, - "paper_bgcolor": "rgba(0,0,0,0)", - "plot_bgcolor": "rgba(0,0,0,0)", - "autosize": True, - }, - } - return new_figure - - -# Running the server -if __name__ == "__main__": - app.run_server(debug=True, port=8050) +suffix_indicator = "_indicator" \ No newline at end of file diff --git a/apps/dash-manufacture-spc-dashboard/assets/base-styles.css b/apps/dash-manufacture-spc-dashboard/assets/css/base-styles.css similarity index 100% rename from apps/dash-manufacture-spc-dashboard/assets/base-styles.css rename to apps/dash-manufacture-spc-dashboard/assets/css/base-styles.css diff --git a/apps/dash-manufacture-spc-dashboard/assets/fonts.css b/apps/dash-manufacture-spc-dashboard/assets/css/fonts.css similarity index 100% rename from apps/dash-manufacture-spc-dashboard/assets/fonts.css rename to apps/dash-manufacture-spc-dashboard/assets/css/fonts.css diff --git a/apps/dash-manufacture-spc-dashboard/assets/spc-custom-styles.css b/apps/dash-manufacture-spc-dashboard/assets/css/spc-custom-styles.css similarity index 100% rename from apps/dash-manufacture-spc-dashboard/assets/spc-custom-styles.css rename to apps/dash-manufacture-spc-dashboard/assets/css/spc-custom-styles.css diff --git a/apps/dash-manufacture-spc-dashboard/index.py b/apps/dash-manufacture-spc-dashboard/index.py new file mode 100644 index 000000000..9332bd39a --- /dev/null +++ b/apps/dash-manufacture-spc-dashboard/index.py @@ -0,0 +1,335 @@ +from dash import html, dcc, Dash, Input, Output, State, callback, callback_context, dash_table +import pandas as pd + +from app import app, df, params, state_dict, max_length, suffix_button_id, suffix_sparkline_graph, suffix_count, suffix_ooc_n, suffix_ooc_g, suffix_indicator +from utils.helper_functions import update_sparkline, init_value_setter_store, populate_ooc, update_count +from utils.graphs import generate_graph, build_chart_panel +import utils.components as comp + +app.layout = html.Div( + id="big-app-container", + children=[ + comp.build_banner(), + dcc.Interval( + id="interval-component", + interval=2 * 1000, # in milliseconds + n_intervals=50, # start at batch 50 + disabled=True, + ), + html.Div( + id="app-container", + children=[ + comp.build_tabs(), + # Main app + html.Div(id="app-content"), + ], + ), + dcc.Store(id="value-setter-store", data=init_value_setter_store(df)), + dcc.Store(id="n-interval-stage", data=50), + comp.generate_modal(), + ], +) + + +@callback( + Output("app-content", "children"), + Output("interval-component", "n_intervals"), + Input("app-tabs", "value"), + State("n-interval-stage", "data"), +) +def render_tab_content(tab_switch, stopped_interval): + if tab_switch == "tab1": + return comp.build_tab_1(), stopped_interval + return html.Div( + id="status-container", + children=[ + comp.build_quick_stats_panel(), + html.Div( + id="graphs-container", + children=[comp.build_top_panel(stopped_interval), build_chart_panel()], + ), + ], + ), stopped_interval + + +@callback( + Output("n-interval-stage", "data"), + Input("app-tabs", "value"), + State("interval-component", "n_intervals"), + State("interval-component", "disabled"), + State("n-interval-stage", "data"), +) +def update_interval_state(tab_switch, cur_interval, disabled, cur_stage): + if disabled: + return cur_interval + + if tab_switch == "tab1": + return cur_interval + return cur_stage + + +@callback( + Output("interval-component", "disabled"), + Output("stop-button", "buttonText"), + Input("stop-button", "n_clicks"), + State("interval-component", "disabled"), +) +def stop_production(n_clicks, current): + """ + Callbacks for stopping interval update + """ + if n_clicks == 0: + return True, "start" + return not current, "stop" if current else "start" + + +@callback( + Output("markdown", "style"), + Input("learn-more-button", "n_clicks"), + Input("markdown_close", "n_clicks"), +) +def update_click_output(button_click, close_click): + """ + Callbacks for modal popup + """ + ctx = callback_context + + if ctx.triggered: + prop_id = ctx.triggered[0]["prop_id"].split(".")[0] + if prop_id == "learn-more-button": + return {"display": "block"} + + return {"display": "none"} + + +@callback( + Output("progress-gauge", "value"), + Input("interval-component", "n_intervals"), +) +def update_gauge(interval): + """ + Update progress gauge + """ + if interval < max_length: + total_count = interval + else: + total_count = max_length + + return int(total_count) + + +@callback( + Output("ud_usl_input", "value"), + Output("ud_lsl_input", "value"), + Output("ud_ucl_input", "value"), + Output("ud_lcl_input", "value"), + Input("metric-select-dropdown", "value"), + State("value-setter-store", "data"), +) +def build_value_setter_panel(dd_select, state_value): + """ + Update values based on store data and dropdown selection + """ + return state_value[dd_select]["usl"], state_value[dd_select]["lsl"], state_value[dd_select]["ucl"], state_value[dd_select]["lcl"], + + +@callback( + Output("value-setter-store", "data"), + Input("value-setter-set-btn", "n_clicks"), + State("metric-select-dropdown", "value"), + State("value-setter-store", "data"), + State("ud_usl_input", "value"), + State("ud_lsl_input", "value"), + State("ud_ucl_input", "value"), + State("ud_lcl_input", "value"), +) +def set_value_setter_store(set_btn, param, data, usl, lsl, ucl, lcl): + """ + Update stored data via click and recalculate ooc in case of param updates + """ + if set_btn is not None: + data[param]["usl"] = usl + data[param]["lsl"] = lsl + data[param]["ucl"] = ucl + data[param]["lcl"] = lcl + + data[param]["ooc"] = populate_ooc(df[param], ucl, lcl) + return data + + +@callback( + Output("value-setter-view-output", "children"), + Input("value-setter-view-btn", "n_clicks"), + Input("metric-select-dropdown", "value"), + Input("value-setter-store", "data"), +) +def show_current_specs(n_clicks, dd_select, store_data): + if n_clicks > 0: + curr_col_data = store_data[dd_select] + new_df_dict = { + "Specs": [ + "Upper Specification Limit", + "Lower Specification Limit", + "Upper Control Limit", + "Lower Control Limit", + ], + "Current Setup": [ + curr_col_data["usl"], + curr_col_data["lsl"], + curr_col_data["ucl"], + curr_col_data["lcl"], + ], + } + new_df = pd.DataFrame.from_dict(new_df_dict) + return dash_table.DataTable( + style_header={"fontWeight": "bold", "color": "inherit"}, + style_as_list_view=True, + fill_width=True, + style_cell_conditional=[ + {"if": {"column_id": "Specs"}, "textAlign": "left"} + ], + style_cell={ + "backgroundColor": "#1e2130", + "fontFamily": "Open Sans", + "padding": "0 2rem", + "color": "darkgray", + "border": "none", + }, + css=[ + {"selector": "tr:hover td", "rule": "color: #91dfd2 !important;"}, + {"selector": "td", "rule": "border: none !important;"}, + { + "selector": ".dash-cell.focused", + "rule": "background-color: #1e2130 !important;", + }, + {"selector": "table", "rule": "--accent: #1e2130;"}, + {"selector": "tr", "rule": "background-color: transparent"}, + ], + data=new_df.to_dict("rows"), + columns=[{"id": c, "name": c} for c in ["Specs", "Current Setup"]], + ) + + +# decorator for list of output +def create_callback(param): + def callback(interval, stored_data): + count, ooc_n, ooc_g_value, indicator = update_count(max_length, interval, param, stored_data) + spark_line_data = update_sparkline(state_dict, max_length, interval, param) + return count, spark_line_data, ooc_n, ooc_g_value, indicator + + return callback + + +for param in params[1:]: + update_param_row_function = create_callback(param) + callback( + output=[ + Output(param + suffix_count, "children"), + Output(param + suffix_sparkline_graph, "extendData"), + Output(param + suffix_ooc_n, "children"), + Output(param + suffix_ooc_g, "value"), + Output(param + suffix_indicator, "color"), + ], + inputs=[Input("interval-component", "n_intervals")], + state=[State("value-setter-store", "data")], + )(update_param_row_function) + + +@callback( + Output("control-chart-live", "figure"), + Input("interval-component", "n_intervals"), + Input(params[1] + suffix_button_id, "n_clicks"), + Input(params[2] + suffix_button_id, "n_clicks"), + Input(params[3] + suffix_button_id, "n_clicks"), + Input(params[4] + suffix_button_id, "n_clicks"), + Input(params[5] + suffix_button_id, "n_clicks"), + Input(params[6] + suffix_button_id, "n_clicks"), + Input(params[7] + suffix_button_id, "n_clicks"), + State("value-setter-store", "data"), + State("control-chart-live", "figure"), +) +def update_control_chart(interval, n1, n2, n3, n4, n5, n6, n7, data, cur_fig): + """ + button to choose/update figure based on click + """ + ctx = callback_context # Find which one has been triggered + + if not ctx.triggered: + return generate_graph(interval, data, params[1]) + + if ctx.triggered: + # Get most recently triggered id and prop_type + splitted = ctx.triggered[0]["prop_id"].split(".") + prop_id = splitted[0] + prop_type = splitted[1] + + if prop_type == "n_clicks": + curr_id = cur_fig["data"][0]["name"] + prop_id = prop_id[:-7] + if curr_id == prop_id: + return generate_graph(interval, data, curr_id) + else: + return generate_graph(interval, data, prop_id) + + if prop_type == "n_intervals" and cur_fig is not None: + curr_id = cur_fig["data"][0]["name"] + return generate_graph(interval, data, curr_id) + + +@callback( + Output("piechart", "figure"), + Input("interval-component", "n_intervals"), + State("value-setter-store", "data"), +) +def update_piechart(interval, stored_data): + if interval == 0: + return { + "data": [], + "layout": { + "font": {"color": "white"}, + "paper_bgcolor": "rgba(0,0,0,0)", + "plot_bgcolor": "rgba(0,0,0,0)", + }, + } + + if interval >= max_length: + total_count = max_length - 1 + else: + total_count = interval - 1 + + values = [] + colors = [] + for param in params[1:]: + ooc_param = (stored_data[param]["ooc"][total_count] * 100) + 1 + values.append(ooc_param) + if ooc_param > 6: + colors.append("#f45060") + else: + colors.append("#91dfd2") + + new_figure = { + "data": [ + { + "labels": params[1:], + "values": values, + "type": "pie", + "marker": {"colors": colors, "line": dict(color="white", width=2)}, + "hoverinfo": "label", + "textinfo": "label", + } + ], + "layout": { + "margin": dict(t=20, b=50), + "uirevision": True, + "font": {"color": "white"}, + "showlegend": False, + "paper_bgcolor": "rgba(0,0,0,0)", + "plot_bgcolor": "rgba(0,0,0,0)", + "autosize": True, + }, + } + return new_figure + + +if __name__ == "__main__": + app.run_server(debug=True) diff --git a/apps/dash-manufacture-spc-dashboard/requirements.txt b/apps/dash-manufacture-spc-dashboard/requirements.txt index 547141681..ae3703627 100644 --- a/apps/dash-manufacture-spc-dashboard/requirements.txt +++ b/apps/dash-manufacture-spc-dashboard/requirements.txt @@ -1,5 +1,5 @@ -dash==1.0.2 -dash-daq==0.1.7 -gunicorn>=19.9.0 -numpy>=1.16.2 -pandas>=0.24.2 +dash==2.4.1 +dash-daq==0.5.0 +numpy==1.22.3 +pandas==1.4.2 +gunicorn==20.1.0 \ No newline at end of file diff --git a/apps/dash-manufacture-spc-dashboard/runtime.txt b/apps/dash-manufacture-spc-dashboard/runtime.txt new file mode 100644 index 000000000..cfa660c42 --- /dev/null +++ b/apps/dash-manufacture-spc-dashboard/runtime.txt @@ -0,0 +1 @@ +python-3.8.0 \ No newline at end of file diff --git a/apps/dash-manufacture-spc-dashboard/utils/components.py b/apps/dash-manufacture-spc-dashboard/utils/components.py new file mode 100644 index 000000000..2c7287e29 --- /dev/null +++ b/apps/dash-manufacture-spc-dashboard/utils/components.py @@ -0,0 +1,465 @@ +from dash import html, dcc, get_asset_url +import dash_daq as daq +import plotly.graph_objs as go + +from app import params, max_length, state_dict, suffix_row, suffix_button_id, suffix_sparkline_graph, suffix_count, suffix_ooc_n, suffix_ooc_g, suffix_indicator + + +ud_usl_input = daq.NumericInput(id="ud_usl_input", className="setting-input", size=200, max=9999999) +ud_lsl_input = daq.NumericInput(id="ud_lsl_input", className="setting-input", size=200, max=9999999) +ud_ucl_input = daq.NumericInput(id="ud_ucl_input", className="setting-input", size=200, max=9999999) +ud_lcl_input = daq.NumericInput(id="ud_lcl_input", className="setting-input", size=200, max=9999999) + +def build_banner(): + return html.Div( + id="banner", + className="banner", + children=[ + html.Div( + id="banner-text", + children=[ + html.H5("Manufacturing SPC Dashboard"), + html.H6("Process Control and Exception Reporting"), + ], + ), + html.Div( + id="banner-logo", + children=[ + html.A( + html.Button(children="ENTERPRISE DEMO"), + href="https://plotly.com/get-demo/", + ), + html.Button( + id="learn-more-button", children="LEARN MORE", n_clicks=0 + ), + html.A( + html.Img(id="logo", src=get_asset_url("dash-logo-new.png")), + href="https://plotly.com/dash/", + ), + ], + ), + ], + ) + + +def build_tabs(): + return html.Div( + id="tabs", + className="tabs", + children=[ + dcc.Tabs( + id="app-tabs", + value="tab2", + className="custom-tabs", + children=[ + dcc.Tab( + id="Specs-tab", + label="Specification Settings", + value="tab1", + className="custom-tab", + selected_className="custom-tab--selected", + ), + dcc.Tab( + id="Control-chart-tab", + label="Control Charts Dashboard", + value="tab2", + className="custom-tab", + selected_className="custom-tab--selected", + ), + ], + ) + ], + ) + + +def build_tab_1(): + return [ + # Manually select metrics + html.Div( + id="set-specs-intro-container", + children=html.P( + "Use historical control limits to establish a benchmark, or set new values." + ), + ), + html.Div( + id="settings-menu", + children=[ + html.Div( + id="metric-select-menu", + children=[ + html.Label(id="metric-select-title", children="Select Metrics"), + html.Br(), + dcc.Dropdown( + id="metric-select-dropdown", + options=list( + {"label": param, "value": param} for param in params[1:] + ), + value=params[1], + ), + ], + ), + html.Div( + id="value-setter-menu", + children=[ + html.Div( + [ + build_value_setter_line( + "value-setter-panel-header", + "Specs", + "Historical Value", + "Set new value", + ), + build_value_setter_line( + "value-setter-panel-usl", + "Upper Specification limit", + state_dict[params[1]]["usl"], + ud_usl_input, + ), + build_value_setter_line( + "value-setter-panel-lsl", + "Lower Specification limit", + state_dict[params[1]]["lsl"], + ud_lsl_input, + ), + build_value_setter_line( + "value-setter-panel-ucl", + "Upper Control limit", + state_dict[params[1]]["ucl"], + ud_ucl_input, + ), + build_value_setter_line( + "value-setter-panel-lcl", + "Lower Control limit", + state_dict[params[1]]["lcl"], + ud_lcl_input, + ), + ], + ), + html.Br(), + html.Div( + id="button-div", + children=[ + html.Button("Update", id="value-setter-set-btn"), + html.Button( + "View current setup", + id="value-setter-view-btn", + n_clicks=0, + ), + ], + ), + html.Div( + id="value-setter-view-output", className="output-datatable" + ), + ], + ), + ], + ), + ] + + +def build_value_setter_line(line_num, label, value, col3): + return html.Div( + id=line_num, + children=[ + html.Label(label, className="four columns"), + html.Label(value, className="four columns"), + html.Div(col3, className="four columns"), + ], + className="row", + ) + + +def generate_modal(): + return html.Div( + id="markdown", + className="modal", + children=( + html.Div( + id="markdown-container", + className="markdown-container", + children=[ + html.Div( + className="close-container", + children=html.Button( + "Close", + id="markdown_close", + n_clicks=0, + className="closeButton", + ), + ), + html.Div( + className="markdown-text", + children=dcc.Markdown( + children=( + """ + ###### What is this mock app about? + + This is a dashboard for monitoring real-time process quality along manufacture production line. + + ###### What does this app shows + + Click on buttons in `Parameter` column to visualize details of measurement trendlines on the bottom panel. + + The sparkline on top panel and control chart on bottom panel show Shewhart process monitor using mock data. + The trend is updated every other second to simulate real-time measurements. Data falling outside of six-sigma control limit are signals indicating 'Out of Control(OOC)', and will + trigger alerts instantly for a detailed checkup. + + Operators may stop measurement by clicking on `Stop` button, and edit specification parameters by clicking specification tab. + + ###### Source Code + + You can find the source code of this app on our [Github repository](https://github.com/plotly/dash-sample-apps/tree/main/apps/dash-manufacture-spc-dashboard). + + """ + ) + ), + ), + ], + ) + ), + ) + + +def build_quick_stats_panel(): + return html.Div( + id="quick-stats", + className="row", + children=[ + html.Div( + id="card-1", + children=[ + html.P("Operator ID"), + daq.LEDDisplay( + id="operator-led", + value="1704", + color="#92e0d3", + backgroundColor="#1e2130", + size=50, + ), + ], + ), + html.Div( + id="card-2", + children=[ + html.P("Time to completion"), + daq.Gauge( + id="progress-gauge", + max=max_length * 2, + min=0, + showCurrentValue=True, # default size 200 pixel + ), + ], + ), + html.Div( + id="utility-card", + children=[daq.StopButton(id="stop-button", size=160, n_clicks=0)], + ), + ], + ) + + +def generate_section_banner(title): + return html.Div(className="section-banner", children=title) + + +def build_top_panel(stopped_interval): + from utils.graphs import generate_piechart + return html.Div( + id="top-section-container", + className="row", + children=[ + # Metrics summary + html.Div( + id="metric-summary-session", + className="eight columns", + children=[ + generate_section_banner("Process Control Metrics Summary"), + html.Div( + id="metric-div", + children=[ + generate_metric_list_header(), + html.Div( + id="metric-rows", + children=[ + generate_metric_row_helper(stopped_interval, 1), + generate_metric_row_helper(stopped_interval, 2), + generate_metric_row_helper(stopped_interval, 3), + generate_metric_row_helper(stopped_interval, 4), + generate_metric_row_helper(stopped_interval, 5), + generate_metric_row_helper(stopped_interval, 6), + generate_metric_row_helper(stopped_interval, 7), + ], + ), + ], + ), + ], + ), + # Piechart + html.Div( + id="ooc-piechart-outer", + className="four columns", + children=[ + generate_section_banner("% OOC per Parameter"), + generate_piechart(), + ], + ), + ], + ) + + +def generate_metric_list_header(): + return generate_metric_row( + "metric_header", + {"height": "3rem", "margin": "1rem 0", "textAlign": "center"}, + {"id": "m_header_1", "children": html.Div("Parameter")}, + {"id": "m_header_2", "children": html.Div("Count")}, + {"id": "m_header_3", "children": html.Div("Sparkline")}, + {"id": "m_header_4", "children": html.Div("OOC%")}, + {"id": "m_header_5", "children": html.Div("%OOC")}, + {"id": "m_header_6", "children": "Pass/Fail"}, + ) + +def generate_metric_row_helper(stopped_interval, index): + item = params[index] + + div_id = item + suffix_row + button_id = item + suffix_button_id + sparkline_graph_id = item + suffix_sparkline_graph + count_id = item + suffix_count + ooc_percentage_id = item + suffix_ooc_n + ooc_graph_id = item + suffix_ooc_g + indicator_id = item + suffix_indicator + + return generate_metric_row( + div_id, + None, + { + "id": item, + "className": "metric-row-button-text", + "children": html.Button( + id=button_id, + className="metric-row-button", + children=item, + title="Click to visualize live SPC chart", + n_clicks=0, + ), + }, + {"id": count_id, "children": "0"}, + { + "id": item + "_sparkline", + "children": dcc.Graph( + id=sparkline_graph_id, + style={"width": "100%", "height": "95%"}, + config={ + "staticPlot": False, + "editable": False, + "displayModeBar": False, + }, + figure=go.Figure( + { + "data": [ + { + "x": state_dict["Batch"]["data"].tolist()[ + :stopped_interval + ], + "y": state_dict[item]["data"][:stopped_interval], + "mode": "lines+markers", + "name": item, + "line": {"color": "#f4d44d"}, + } + ], + "layout": { + "uirevision": True, + "margin": dict(l=0, r=0, t=4, b=4, pad=0), + "xaxis": dict( + showline=False, + showgrid=False, + zeroline=False, + showticklabels=False, + ), + "yaxis": dict( + showline=False, + showgrid=False, + zeroline=False, + showticklabels=False, + ), + "paper_bgcolor": "rgba(0,0,0,0)", + "plot_bgcolor": "rgba(0,0,0,0)", + }, + } + ), + ), + }, + {"id": ooc_percentage_id, "children": "0.00%"}, + { + "id": ooc_graph_id + "_container", + "children": daq.GraduatedBar( + id=ooc_graph_id, + color={ + "ranges": { + "#92e0d3": [0, 3], + "#f4d44d ": [3, 7], + "#f45060": [7, 15], + } + }, + showCurrentValue=False, + max=15, + value=0, + ), + }, + { + "id": item + "_pf", + "children": daq.Indicator( + id=indicator_id, value=True, color="#91dfd2", size=12 + ), + }, + ) + + +def generate_metric_row(id, style, col1, col2, col3, col4, col5, col6): + if style is None: + style = {"height": "8rem", "width": "100%"} + + return html.Div( + id=id, + className="row metric-row", + style=style, + children=[ + html.Div( + id=col1["id"], + className="one column", + style={"margin-right": "2.5rem", "minWidth": "50px"}, + children=col1["children"], + ), + html.Div( + id=col2["id"], + style={"textAlign": "center"}, + className="one column", + children=col2["children"], + ), + html.Div( + id=col3["id"], + style={"height": "100%"}, + className="four columns", + children=col3["children"], + ), + html.Div( + id=col4["id"], + style={}, + className="one column", + children=col4["children"], + ), + html.Div( + id=col5["id"], + style={"height": "100%", "margin-top": "5rem"}, + className="three columns", + children=col5["children"], + ), + html.Div( + id=col6["id"], + style={"display": "flex", "justifyContent": "center"}, + className="one column", + children=col6["children"], + ), + ], + ) diff --git a/apps/dash-manufacture-spc-dashboard/utils/graphs.py b/apps/dash-manufacture-spc-dashboard/utils/graphs.py new file mode 100644 index 000000000..c261f4e4f --- /dev/null +++ b/apps/dash-manufacture-spc-dashboard/utils/graphs.py @@ -0,0 +1,270 @@ +import plotly.graph_objs as go +from dash import html, dcc + +from utils.components import generate_section_banner + +from app import params, state_dict, max_length + +def generate_piechart(): + return dcc.Graph( + id="piechart", + figure={ + "data": [ + { + "labels": [], + "values": [], + "type": "pie", + "marker": {"line": {"color": "white", "width": 1}}, + "hoverinfo": "label", + "textinfo": "label", + } + ], + "layout": { + "margin": dict(l=20, r=20, t=20, b=20), + "showlegend": True, + "paper_bgcolor": "rgba(0,0,0,0)", + "plot_bgcolor": "rgba(0,0,0,0)", + "font": {"color": "white"}, + "autosize": True, + }, + }, + ) + + + + +def build_chart_panel(): + return html.Div( + id="control-chart-container", + className="twelve columns", + children=[ + generate_section_banner("Live SPC Chart"), + dcc.Graph( + id="control-chart-live", + figure=go.Figure( + { + "data": [ + { + "x": [], + "y": [], + "mode": "lines+markers", + "name": params[1], + } + ], + "layout": { + "paper_bgcolor": "rgba(0,0,0,0)", + "plot_bgcolor": "rgba(0,0,0,0)", + "xaxis": dict( + showline=False, showgrid=False, zeroline=False + ), + "yaxis": dict( + showgrid=False, showline=False, zeroline=False + ), + "autosize": True, + }, + } + ), + ), + ], + ) + + +def generate_graph(interval, specs_dict, col): + stats = state_dict[col] + col_data = stats["data"] + mean = stats["mean"] + ucl = specs_dict[col]["ucl"] + lcl = specs_dict[col]["lcl"] + usl = specs_dict[col]["usl"] + lsl = specs_dict[col]["lsl"] + + x_array = state_dict["Batch"]["data"].tolist() + y_array = col_data.tolist() + + total_count = 0 + + if interval > max_length: + total_count = max_length - 1 + elif interval > 0: + total_count = interval + + ooc_trace = { + "x": [], + "y": [], + "name": "Out of Control", + "mode": "markers", + "marker": dict(color="rgba(210, 77, 87, 0.7)", symbol="square", size=11), + } + + for index, data in enumerate(y_array[:total_count]): + if data >= ucl or data <= lcl: + ooc_trace["x"].append(index + 1) + ooc_trace["y"].append(data) + + histo_trace = { + "x": x_array[:total_count], + "y": y_array[:total_count], + "type": "histogram", + "orientation": "h", + "name": "Distribution", + "xaxis": "x2", + "yaxis": "y2", + "marker": {"color": "#f4d44d"}, + } + + fig = { + "data": [ + { + "x": x_array[:total_count], + "y": y_array[:total_count], + "mode": "lines+markers", + "name": col, + "line": {"color": "#f4d44d"}, + }, + ooc_trace, + histo_trace, + ] + } + + len_figure = len(fig["data"][0]["x"]) + + fig["layout"] = dict( + margin=dict(t=40), + hovermode="closest", + uirevision=col, + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + legend={"font": {"color": "darkgray"}, "orientation": "h", "x": 0, "y": 1.1}, + font={"color": "darkgray"}, + showlegend=True, + xaxis={ + "zeroline": False, + "showgrid": False, + "title": "Batch Number", + "showline": False, + "domain": [0, 0.8], + "titlefont": {"color": "darkgray"}, + }, + yaxis={ + "title": col, + "showgrid": False, + "showline": False, + "zeroline": False, + "autorange": True, + "titlefont": {"color": "darkgray"}, + }, + annotations=[ + { + "x": 0.75, + "y": lcl, + "xref": "paper", + "yref": "y", + "text": "LCL:" + str(round(lcl, 3)), + "showarrow": False, + "font": {"color": "white"}, + }, + { + "x": 0.75, + "y": ucl, + "xref": "paper", + "yref": "y", + "text": "UCL: " + str(round(ucl, 3)), + "showarrow": False, + "font": {"color": "white"}, + }, + { + "x": 0.75, + "y": usl, + "xref": "paper", + "yref": "y", + "text": "USL: " + str(round(usl, 3)), + "showarrow": False, + "font": {"color": "white"}, + }, + { + "x": 0.75, + "y": lsl, + "xref": "paper", + "yref": "y", + "text": "LSL: " + str(round(lsl, 3)), + "showarrow": False, + "font": {"color": "white"}, + }, + { + "x": 0.75, + "y": mean, + "xref": "paper", + "yref": "y", + "text": "Targeted mean: " + str(round(mean, 3)), + "showarrow": False, + "font": {"color": "white"}, + }, + ], + shapes=[ + { + "type": "line", + "xref": "x", + "yref": "y", + "x0": 1, + "y0": usl, + "x1": len_figure + 1, + "y1": usl, + "line": {"color": "#91dfd2", "width": 1, "dash": "dot"}, + }, + { + "type": "line", + "xref": "x", + "yref": "y", + "x0": 1, + "y0": lsl, + "x1": len_figure + 1, + "y1": lsl, + "line": {"color": "#91dfd2", "width": 1, "dash": "dot"}, + }, + { + "type": "line", + "xref": "x", + "yref": "y", + "x0": 1, + "y0": ucl, + "x1": len_figure + 1, + "y1": ucl, + "line": {"color": "rgb(255,127,80)", "width": 1, "dash": "dot"}, + }, + { + "type": "line", + "xref": "x", + "yref": "y", + "x0": 1, + "y0": mean, + "x1": len_figure + 1, + "y1": mean, + "line": {"color": "rgb(255,127,80)", "width": 2}, + }, + { + "type": "line", + "xref": "x", + "yref": "y", + "x0": 1, + "y0": lcl, + "x1": len_figure + 1, + "y1": lcl, + "line": {"color": "rgb(255,127,80)", "width": 1, "dash": "dot"}, + }, + ], + xaxis2={ + "title": "Count", + "domain": [0.8, 1], # 70 to 100 % of width + "titlefont": {"color": "darkgray"}, + "showgrid": False, + }, + yaxis2={ + "anchor": "free", + "overlaying": "y", + "side": "right", + "showticklabels": False, + "titlefont": {"color": "darkgray"}, + }, + ) + + return fig diff --git a/apps/dash-manufacture-spc-dashboard/utils/helper_functions.py b/apps/dash-manufacture-spc-dashboard/utils/helper_functions.py new file mode 100644 index 000000000..e9c501f1c --- /dev/null +++ b/apps/dash-manufacture-spc-dashboard/utils/helper_functions.py @@ -0,0 +1,102 @@ +def init_df(df): + ret = {} + for col in list(df[1:]): + data = df[col] + stats = data.describe() + + std = stats["std"].tolist() + ucl = (stats["mean"] + 3 * stats["std"]).tolist() + lcl = (stats["mean"] - 3 * stats["std"]).tolist() + usl = (stats["mean"] + stats["std"]).tolist() + lsl = (stats["mean"] - stats["std"]).tolist() + + ret.update( + { + col: { + "count": stats["count"].tolist(), + "data": data, + "mean": stats["mean"].tolist(), + "std": std, + "ucl": round(ucl, 3), + "lcl": round(lcl, 3), + "usl": round(usl, 3), + "lsl": round(lsl, 3), + "min": stats["min"].tolist(), + "max": stats["max"].tolist(), + "ooc": populate_ooc(data, ucl, lcl), + } + } + ) + + return ret + + +def init_value_setter_store(df): + # Initialize store data + state_dict = init_df(df) + return state_dict + + +def populate_ooc(data, ucl, lcl): + ooc_count = 0 + ret = [] + for i in range(len(data)): + if data[i] >= ucl or data[i] <= lcl: + ooc_count += 1 + ret.append(ooc_count / (i + 1)) + else: + ret.append(ooc_count / (i + 1)) + return ret + + +def update_sparkline(state_dict, max_length, interval, param): + x_array = state_dict["Batch"]["data"].tolist() + y_array = state_dict[param]["data"].tolist() + + if interval == 0: + x_new = y_new = None + + else: + if interval >= max_length: + total_count = max_length + else: + total_count = interval + x_new = x_array[:total_count][-1] + y_new = y_array[:total_count][-1] + + return dict(x=[[x_new]], y=[[y_new]]), [0] + + +def update_count(max_length, interval, col, data): + if interval == 0: + return "0", "0.00%", 0.00001, "#92e0d3" + + if interval > 0: + + if interval >= max_length: + total_count = max_length - 1 + else: + total_count = interval - 1 + + ooc_percentage_f = data[col]["ooc"][total_count] * 100 + ooc_percentage_str = "%.2f" % ooc_percentage_f + "%" + + # Set maximum ooc to 15 for better grad bar display + if ooc_percentage_f > 15: + ooc_percentage_f = 15 + + if ooc_percentage_f == 0.0: + ooc_grad_val = 0.00001 + else: + ooc_grad_val = float(ooc_percentage_f) + + # Set indicator theme according to threshold 5% + if 0 <= ooc_grad_val <= 5: + color = "#92e0d3" + elif 5 < ooc_grad_val < 7: + color = "#f4d44d" + else: + color = "#FF0000" + + return str(total_count + 1), ooc_percentage_str, ooc_grad_val, color + From 2b905727a5bc05e8d74bfa256c6f44b489103634 Mon Sep 17 00:00:00 2001 From: admin <51248046+danton267@users.noreply.github.com> Date: Thu, 19 May 2022 14:18:02 +0100 Subject: [PATCH 2/3] v2 --- .../dash-manufacture-spc-dashboard/.gitignore | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 apps/dash-manufacture-spc-dashboard/.gitignore diff --git a/apps/dash-manufacture-spc-dashboard/.gitignore b/apps/dash-manufacture-spc-dashboard/.gitignore new file mode 100644 index 000000000..3ebde9df2 --- /dev/null +++ b/apps/dash-manufacture-spc-dashboard/.gitignore @@ -0,0 +1,190 @@ +# .gitignore specifies the files that shouldn't be included +# in version control and therefore shouldn't be included when +# deploying an application to Dash Enterprise +# This is a very exhaustive list! +# This list was based off of https://github.com/github/gitignore + +# Ignore data that is generated during the runtime of an application +# This folder is used by the "Large Data" sample applications +runtime_data/ + +# Omit SQLite databases that may be produced by dash-snapshots in development +*.db + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + + +# Jupyter Notebook + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + + +# macOS General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# History files +.Rhistory +.Rapp.history + +# Session Data files +.RData + +# User-specific files +.Ruserdata + +# Example code in package build process +*-Ex.R + +# Output files from R CMD check +/*.Rcheck/ + +# RStudio files +.Rproj.user/ + +# produced vignettes +vignettes/*.html +vignettes/*.pdf + +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth + +# knitr and R markdown default cache directories +*_cache/ +/cache/ + +# Temporary files created by R markdown +*.utf8.md +*.knit.md + +# R Environment Variables +.Renviron + +# Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# SublineText +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings \ No newline at end of file From a91c1ff11628dee0920e154e8333edc043bda39e Mon Sep 17 00:00:00 2001 From: admin <51248046+danton267@users.noreply.github.com> Date: Wed, 25 May 2022 15:03:57 +0100 Subject: [PATCH 3/3] updates --- apps/dash-manufacture-spc-dashboard/README.md | 6 +- apps/dash-manufacture-spc-dashboard/app.py | 351 +++++++++++++++++- .../assets/css/header.css | 33 ++ .../assets/dash-logo-new.png | Bin 18588 -> 0 bytes .../{img => assets/github}/screencapture1.png | Bin .../{img => assets/github}/screencapture2.png | Bin .../assets/images/plotly-logo.png | Bin 0 -> 23021 bytes .../assets/plotly_logo.png | Bin 6820 -> 0 bytes .../constants.py | 18 + apps/dash-manufacture-spc-dashboard/index.py | 335 ----------------- .../utils/components.py | 25 +- .../utils/graphs.py | 3 +- 12 files changed, 395 insertions(+), 376 deletions(-) create mode 100644 apps/dash-manufacture-spc-dashboard/assets/css/header.css delete mode 100644 apps/dash-manufacture-spc-dashboard/assets/dash-logo-new.png rename apps/dash-manufacture-spc-dashboard/{img => assets/github}/screencapture1.png (100%) rename apps/dash-manufacture-spc-dashboard/{img => assets/github}/screencapture2.png (100%) create mode 100644 apps/dash-manufacture-spc-dashboard/assets/images/plotly-logo.png delete mode 100644 apps/dash-manufacture-spc-dashboard/assets/plotly_logo.png create mode 100644 apps/dash-manufacture-spc-dashboard/constants.py delete mode 100644 apps/dash-manufacture-spc-dashboard/index.py diff --git a/apps/dash-manufacture-spc-dashboard/README.md b/apps/dash-manufacture-spc-dashboard/README.md index c8abd396a..54e07de41 100644 --- a/apps/dash-manufacture-spc-dashboard/README.md +++ b/apps/dash-manufacture-spc-dashboard/README.md @@ -5,9 +5,9 @@ This is a demo of Dash interactive Python framework developed by [Plotly](https//plot.ly/). ## Screenshots -![initial](img/screencapture2.png) +![initial](assets/github/screencapture2.png) -![initial](img/screencapture1.png) +![initial](assets/github/screencapture1.png) ## Built With @@ -66,3 +66,5 @@ Operators may stop measurement by clicking `Stop` button, and edit specification * [Shewhart statistical process control](https://en.wikipedia.org/wiki/Shewhart_individuals_control_chart) * [Dash User Guide](https://dash.plot.ly/) +## App worked on by +* **Daniel Anton S** - *2022 complete refactor* - [@danton267](https://github.com/danton267) \ No newline at end of file diff --git a/apps/dash-manufacture-spc-dashboard/app.py b/apps/dash-manufacture-spc-dashboard/app.py index 88e39a092..83fc67365 100644 --- a/apps/dash-manufacture-spc-dashboard/app.py +++ b/apps/dash-manufacture-spc-dashboard/app.py @@ -1,28 +1,341 @@ -from dash import Dash +from dash import Dash, html, dcc, Dash, Input, Output, State, callback, callback_context, dash_table import pandas as pd -from utils.helper_functions import init_df + +from constants import df, params, state_dict, max_length, suffix_button_id, suffix_sparkline_graph, suffix_count, suffix_ooc_n, suffix_ooc_g, suffix_indicator +from utils.helper_functions import update_sparkline, init_value_setter_store, populate_ooc, update_count +from utils.graphs import generate_graph, build_chart_panel +import utils.components as comp app = Dash( __name__, - meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}], title="Manufacturing SPC Dashboard", - suppress_callback_exceptions=True, update_title=None ) server = app.server -## Load data used across the app -df = pd.read_csv("data/spc_data.csv") -state_dict = init_df(df) - -## Define constants used across the app -params = list(df) -max_length = len(df) - -suffix_row = "_row" -suffix_button_id = "_button" -suffix_sparkline_graph = "_sparkline_graph" -suffix_count = "_count" -suffix_ooc_n = "_OOC_number" -suffix_ooc_g = "_OOC_graph" -suffix_indicator = "_indicator" \ No newline at end of file +app.layout = html.Div( + id="big-app-container", + children=[ + comp.build_banner(), + dcc.Interval( + id="interval-component", + interval=2 * 1000, # in milliseconds + n_intervals=50, # start at batch 50 + disabled=True, + ), + html.Div( + id="app-container", + children=[ + comp.build_tabs(), + html.Div(id="app-content"), # Main app + ], + ), + dcc.Store(id="value-setter-store", data=init_value_setter_store(df)), + dcc.Store(id="n-interval-stage", data=50), + comp.generate_modal(), + ], +) + + +@callback( + Output("app-content", "children"), + Output("interval-component", "n_intervals"), + Input("app-tabs", "value"), + State("n-interval-stage", "data"), +) +def render_tab_content(tab_switch, stopped_interval): + if tab_switch == "tab1": + return comp.build_tab_1(), stopped_interval + return html.Div( + id="status-container", + children=[ + comp.build_quick_stats_panel(), + html.Div( + id="graphs-container", + children=[comp.build_top_panel(stopped_interval), build_chart_panel()], + ), + ], + ), stopped_interval + + +@callback( + Output("n-interval-stage", "data"), + Input("app-tabs", "value"), + State("interval-component", "n_intervals"), + State("interval-component", "disabled"), + State("n-interval-stage", "data"), +) +def update_interval_state(tab_switch, cur_interval, disabled, cur_stage): + if disabled: + return cur_interval + + if tab_switch == "tab1": + return cur_interval + return cur_stage + + +@callback( + Output("interval-component", "disabled"), + Output("stop-button", "buttonText"), + Input("stop-button", "n_clicks"), + State("interval-component", "disabled"), +) +def stop_production(n_clicks, current): + """ + Callbacks for stopping interval update + """ + if n_clicks == 0: + return True, "start" + return not current, "stop" if current else "start" + + +@callback( + Output("markdown", "style"), + Input("learn-more-button", "n_clicks"), + Input("markdown_close", "n_clicks"), +) +def update_click_output(button_click, close_click): + """ + Callbacks for modal popup + """ + ctx = callback_context + + if ctx.triggered: + prop_id = ctx.triggered[0]["prop_id"].split(".")[0] + if prop_id == "learn-more-button": + return {"display": "block"} + + return {"display": "none"} + + +@callback( + Output("progress-gauge", "value"), + Input("interval-component", "n_intervals"), +) +def update_gauge(interval): + """ + Update progress gauge + """ + if interval < max_length: + total_count = interval + else: + total_count = max_length + + return int(total_count) + + +@callback( + Output("ud_usl_input", "value"), + Output("ud_lsl_input", "value"), + Output("ud_ucl_input", "value"), + Output("ud_lcl_input", "value"), + Input("metric-select-dropdown", "value"), + State("value-setter-store", "data"), +) +def build_value_setter_panel(dd_select, state_value): + """ + Update values based on store data and dropdown selection + """ + return state_value[dd_select]["usl"], state_value[dd_select]["lsl"], state_value[dd_select]["ucl"], state_value[dd_select]["lcl"], + + +@callback( + Output("value-setter-store", "data"), + Input("value-setter-set-btn", "n_clicks"), + State("metric-select-dropdown", "value"), + State("value-setter-store", "data"), + State("ud_usl_input", "value"), + State("ud_lsl_input", "value"), + State("ud_ucl_input", "value"), + State("ud_lcl_input", "value"), +) +def set_value_setter_store(set_btn, param, data, usl, lsl, ucl, lcl): + """ + Update stored data via click and recalculate ooc in case of param updates + """ + if set_btn is not None: + data[param]["usl"] = usl + data[param]["lsl"] = lsl + data[param]["ucl"] = ucl + data[param]["lcl"] = lcl + + data[param]["ooc"] = populate_ooc(df[param], ucl, lcl) + return data + + +@callback( + Output("value-setter-view-output", "children"), + Input("value-setter-view-btn", "n_clicks"), + Input("metric-select-dropdown", "value"), + Input("value-setter-store", "data"), +) +def show_current_specs(n_clicks, dd_select, store_data): + if n_clicks > 0: + curr_col_data = store_data[dd_select] + new_df_dict = { + "Specs": [ + "Upper Specification Limit", + "Lower Specification Limit", + "Upper Control Limit", + "Lower Control Limit", + ], + "Current Setup": [ + curr_col_data["usl"], + curr_col_data["lsl"], + curr_col_data["ucl"], + curr_col_data["lcl"], + ], + } + new_df = pd.DataFrame.from_dict(new_df_dict) + return dash_table.DataTable( + style_header={"fontWeight": "bold", "color": "inherit"}, + style_as_list_view=True, + fill_width=True, + style_cell_conditional=[ + {"if": {"column_id": "Specs"}, "textAlign": "left"} + ], + style_cell={ + "backgroundColor": "#1e2130", + "fontFamily": "Open Sans", + "padding": "0 2rem", + "color": "darkgray", + "border": "none", + }, + css=[ + {"selector": "tr:hover td", "rule": "color: #91dfd2 !important;"}, + {"selector": "td", "rule": "border: none !important;"}, + { + "selector": ".dash-cell.focused", + "rule": "background-color: #1e2130 !important;", + }, + {"selector": "table", "rule": "--accent: #1e2130;"}, + {"selector": "tr", "rule": "background-color: transparent"}, + ], + data=new_df.to_dict("rows"), + columns=[{"id": c, "name": c} for c in ["Specs", "Current Setup"]], + ) + + +# decorator for list of output +def create_callback(param): + def callback(interval, stored_data): + count, ooc_n, ooc_g_value, indicator = update_count(max_length, interval, param, stored_data) + spark_line_data = update_sparkline(state_dict, max_length, interval, param) + return count, spark_line_data, ooc_n, ooc_g_value, indicator + + return callback + + +for param in params[1:]: + update_param_row_function = create_callback(param) + callback( + output=[ + Output(param + suffix_count, "children"), + Output(param + suffix_sparkline_graph, "extendData"), + Output(param + suffix_ooc_n, "children"), + Output(param + suffix_ooc_g, "value"), + Output(param + suffix_indicator, "color"), + ], + inputs=[Input("interval-component", "n_intervals")], + state=[State("value-setter-store", "data")], + )(update_param_row_function) + + +@callback( + Output("control-chart-live", "figure"), + Input("interval-component", "n_intervals"), + Input(params[1] + suffix_button_id, "n_clicks"), + Input(params[2] + suffix_button_id, "n_clicks"), + Input(params[3] + suffix_button_id, "n_clicks"), + Input(params[4] + suffix_button_id, "n_clicks"), + Input(params[5] + suffix_button_id, "n_clicks"), + Input(params[6] + suffix_button_id, "n_clicks"), + Input(params[7] + suffix_button_id, "n_clicks"), + State("value-setter-store", "data"), + State("control-chart-live", "figure"), +) +def update_control_chart(interval, n1, n2, n3, n4, n5, n6, n7, data, cur_fig): + """ + button to choose/update figure based on click + """ + ctx = callback_context # Find which one has been triggered + + if not ctx.triggered: + return generate_graph(interval, data, params[1]) + + if ctx.triggered: + # Get most recently triggered id and prop_type + splitted = ctx.triggered[0]["prop_id"].split(".") + prop_id = splitted[0] + prop_type = splitted[1] + + if prop_type == "n_clicks": + curr_id = cur_fig["data"][0]["name"] + prop_id = prop_id[:-7] + if curr_id == prop_id: + return generate_graph(interval, data, curr_id) + else: + return generate_graph(interval, data, prop_id) + + if prop_type == "n_intervals" and cur_fig is not None: + curr_id = cur_fig["data"][0]["name"] + return generate_graph(interval, data, curr_id) + + +@callback( + Output("piechart", "figure"), + Input("interval-component", "n_intervals"), + State("value-setter-store", "data"), +) +def update_piechart(interval, stored_data): + if interval == 0: + return { + "data": [], + "layout": { + "font": {"color": "white"}, + "paper_bgcolor": "rgba(0,0,0,0)", + "plot_bgcolor": "rgba(0,0,0,0)", + }, + } + + if interval >= max_length: + total_count = max_length - 1 + else: + total_count = interval - 1 + + values = [] + colors = [] + for param in params[1:]: + ooc_param = (stored_data[param]["ooc"][total_count] * 100) + 1 + values.append(ooc_param) + if ooc_param > 6: + colors.append("#f45060") + else: + colors.append("#91dfd2") + + new_figure = { + "data": [ + { + "labels": params[1:], + "values": values, + "type": "pie", + "marker": {"colors": colors, "line": dict(color="white", width=2)}, + "hoverinfo": "label", + "textinfo": "label", + } + ], + "layout": { + "margin": dict(t=20, b=50), + "uirevision": True, + "font": {"color": "white"}, + "showlegend": False, + "paper_bgcolor": "rgba(0,0,0,0)", + "plot_bgcolor": "rgba(0,0,0,0)", + "autosize": True, + }, + } + return new_figure + + +if __name__ == "__main__": + app.run_server(debug=True) \ No newline at end of file diff --git a/apps/dash-manufacture-spc-dashboard/assets/css/header.css b/apps/dash-manufacture-spc-dashboard/assets/css/header.css new file mode 100644 index 000000000..d6ad0bae1 --- /dev/null +++ b/apps/dash-manufacture-spc-dashboard/assets/css/header.css @@ -0,0 +1,33 @@ +/* Demo button css */ +.demo-button { + font-size: 1.5vh; + font-family: Open Sans,sans-serif; + text-decoration: none; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-radius: 8px; + font-weight: 700; + -webkit-padding-start: 1rem; + padding-inline-start: 1rem; + -webkit-padding-end: 1rem; + padding-inline-end: 1rem; + color: #ffffff; + letter-spacing: 1.5px; + border: solid 1.5px transparent; + box-shadow: 2px 1000px 1px #0c0c0c inset; + background-image: linear-gradient(135deg, #7A76FF, #7A76FF, #7FE4FF); + -webkit-background-size: 200% 100%; + background-size: 200% 100%; + -webkit-background-position: 99%; + background-position: 99%; + background-origin: border-box; + transition: all .4s ease-in-out; + padding-top: 1vh; + padding-bottom: 1vh; +} +.demo-button:hover { + color: #7A76FF; + background-position: 0%; +} \ No newline at end of file diff --git a/apps/dash-manufacture-spc-dashboard/assets/dash-logo-new.png b/apps/dash-manufacture-spc-dashboard/assets/dash-logo-new.png deleted file mode 100644 index 040cde17424f9bd58cec6360eb2d1ace958f0634..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18588 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 ZPPOfa;A_FoPW0FbP*Ko;SHoTg{Re31s#^d6 diff --git a/apps/dash-manufacture-spc-dashboard/constants.py b/apps/dash-manufacture-spc-dashboard/constants.py new file mode 100644 index 000000000..9d6da7dc1 --- /dev/null +++ b/apps/dash-manufacture-spc-dashboard/constants.py @@ -0,0 +1,18 @@ +import pandas as pd +from utils.helper_functions import init_df + +## Load data used across the app +df = pd.read_csv("data/spc_data.csv") +state_dict = init_df(df) + +## Define constants used across the app +params = list(df) +max_length = len(df) + +suffix_row = "_row" +suffix_button_id = "_button" +suffix_sparkline_graph = "_sparkline_graph" +suffix_count = "_count" +suffix_ooc_n = "_OOC_number" +suffix_ooc_g = "_OOC_graph" +suffix_indicator = "_indicator" \ No newline at end of file diff --git a/apps/dash-manufacture-spc-dashboard/index.py b/apps/dash-manufacture-spc-dashboard/index.py deleted file mode 100644 index 9332bd39a..000000000 --- a/apps/dash-manufacture-spc-dashboard/index.py +++ /dev/null @@ -1,335 +0,0 @@ -from dash import html, dcc, Dash, Input, Output, State, callback, callback_context, dash_table -import pandas as pd - -from app import app, df, params, state_dict, max_length, suffix_button_id, suffix_sparkline_graph, suffix_count, suffix_ooc_n, suffix_ooc_g, suffix_indicator -from utils.helper_functions import update_sparkline, init_value_setter_store, populate_ooc, update_count -from utils.graphs import generate_graph, build_chart_panel -import utils.components as comp - -app.layout = html.Div( - id="big-app-container", - children=[ - comp.build_banner(), - dcc.Interval( - id="interval-component", - interval=2 * 1000, # in milliseconds - n_intervals=50, # start at batch 50 - disabled=True, - ), - html.Div( - id="app-container", - children=[ - comp.build_tabs(), - # Main app - html.Div(id="app-content"), - ], - ), - dcc.Store(id="value-setter-store", data=init_value_setter_store(df)), - dcc.Store(id="n-interval-stage", data=50), - comp.generate_modal(), - ], -) - - -@callback( - Output("app-content", "children"), - Output("interval-component", "n_intervals"), - Input("app-tabs", "value"), - State("n-interval-stage", "data"), -) -def render_tab_content(tab_switch, stopped_interval): - if tab_switch == "tab1": - return comp.build_tab_1(), stopped_interval - return html.Div( - id="status-container", - children=[ - comp.build_quick_stats_panel(), - html.Div( - id="graphs-container", - children=[comp.build_top_panel(stopped_interval), build_chart_panel()], - ), - ], - ), stopped_interval - - -@callback( - Output("n-interval-stage", "data"), - Input("app-tabs", "value"), - State("interval-component", "n_intervals"), - State("interval-component", "disabled"), - State("n-interval-stage", "data"), -) -def update_interval_state(tab_switch, cur_interval, disabled, cur_stage): - if disabled: - return cur_interval - - if tab_switch == "tab1": - return cur_interval - return cur_stage - - -@callback( - Output("interval-component", "disabled"), - Output("stop-button", "buttonText"), - Input("stop-button", "n_clicks"), - State("interval-component", "disabled"), -) -def stop_production(n_clicks, current): - """ - Callbacks for stopping interval update - """ - if n_clicks == 0: - return True, "start" - return not current, "stop" if current else "start" - - -@callback( - Output("markdown", "style"), - Input("learn-more-button", "n_clicks"), - Input("markdown_close", "n_clicks"), -) -def update_click_output(button_click, close_click): - """ - Callbacks for modal popup - """ - ctx = callback_context - - if ctx.triggered: - prop_id = ctx.triggered[0]["prop_id"].split(".")[0] - if prop_id == "learn-more-button": - return {"display": "block"} - - return {"display": "none"} - - -@callback( - Output("progress-gauge", "value"), - Input("interval-component", "n_intervals"), -) -def update_gauge(interval): - """ - Update progress gauge - """ - if interval < max_length: - total_count = interval - else: - total_count = max_length - - return int(total_count) - - -@callback( - Output("ud_usl_input", "value"), - Output("ud_lsl_input", "value"), - Output("ud_ucl_input", "value"), - Output("ud_lcl_input", "value"), - Input("metric-select-dropdown", "value"), - State("value-setter-store", "data"), -) -def build_value_setter_panel(dd_select, state_value): - """ - Update values based on store data and dropdown selection - """ - return state_value[dd_select]["usl"], state_value[dd_select]["lsl"], state_value[dd_select]["ucl"], state_value[dd_select]["lcl"], - - -@callback( - Output("value-setter-store", "data"), - Input("value-setter-set-btn", "n_clicks"), - State("metric-select-dropdown", "value"), - State("value-setter-store", "data"), - State("ud_usl_input", "value"), - State("ud_lsl_input", "value"), - State("ud_ucl_input", "value"), - State("ud_lcl_input", "value"), -) -def set_value_setter_store(set_btn, param, data, usl, lsl, ucl, lcl): - """ - Update stored data via click and recalculate ooc in case of param updates - """ - if set_btn is not None: - data[param]["usl"] = usl - data[param]["lsl"] = lsl - data[param]["ucl"] = ucl - data[param]["lcl"] = lcl - - data[param]["ooc"] = populate_ooc(df[param], ucl, lcl) - return data - - -@callback( - Output("value-setter-view-output", "children"), - Input("value-setter-view-btn", "n_clicks"), - Input("metric-select-dropdown", "value"), - Input("value-setter-store", "data"), -) -def show_current_specs(n_clicks, dd_select, store_data): - if n_clicks > 0: - curr_col_data = store_data[dd_select] - new_df_dict = { - "Specs": [ - "Upper Specification Limit", - "Lower Specification Limit", - "Upper Control Limit", - "Lower Control Limit", - ], - "Current Setup": [ - curr_col_data["usl"], - curr_col_data["lsl"], - curr_col_data["ucl"], - curr_col_data["lcl"], - ], - } - new_df = pd.DataFrame.from_dict(new_df_dict) - return dash_table.DataTable( - style_header={"fontWeight": "bold", "color": "inherit"}, - style_as_list_view=True, - fill_width=True, - style_cell_conditional=[ - {"if": {"column_id": "Specs"}, "textAlign": "left"} - ], - style_cell={ - "backgroundColor": "#1e2130", - "fontFamily": "Open Sans", - "padding": "0 2rem", - "color": "darkgray", - "border": "none", - }, - css=[ - {"selector": "tr:hover td", "rule": "color: #91dfd2 !important;"}, - {"selector": "td", "rule": "border: none !important;"}, - { - "selector": ".dash-cell.focused", - "rule": "background-color: #1e2130 !important;", - }, - {"selector": "table", "rule": "--accent: #1e2130;"}, - {"selector": "tr", "rule": "background-color: transparent"}, - ], - data=new_df.to_dict("rows"), - columns=[{"id": c, "name": c} for c in ["Specs", "Current Setup"]], - ) - - -# decorator for list of output -def create_callback(param): - def callback(interval, stored_data): - count, ooc_n, ooc_g_value, indicator = update_count(max_length, interval, param, stored_data) - spark_line_data = update_sparkline(state_dict, max_length, interval, param) - return count, spark_line_data, ooc_n, ooc_g_value, indicator - - return callback - - -for param in params[1:]: - update_param_row_function = create_callback(param) - callback( - output=[ - Output(param + suffix_count, "children"), - Output(param + suffix_sparkline_graph, "extendData"), - Output(param + suffix_ooc_n, "children"), - Output(param + suffix_ooc_g, "value"), - Output(param + suffix_indicator, "color"), - ], - inputs=[Input("interval-component", "n_intervals")], - state=[State("value-setter-store", "data")], - )(update_param_row_function) - - -@callback( - Output("control-chart-live", "figure"), - Input("interval-component", "n_intervals"), - Input(params[1] + suffix_button_id, "n_clicks"), - Input(params[2] + suffix_button_id, "n_clicks"), - Input(params[3] + suffix_button_id, "n_clicks"), - Input(params[4] + suffix_button_id, "n_clicks"), - Input(params[5] + suffix_button_id, "n_clicks"), - Input(params[6] + suffix_button_id, "n_clicks"), - Input(params[7] + suffix_button_id, "n_clicks"), - State("value-setter-store", "data"), - State("control-chart-live", "figure"), -) -def update_control_chart(interval, n1, n2, n3, n4, n5, n6, n7, data, cur_fig): - """ - button to choose/update figure based on click - """ - ctx = callback_context # Find which one has been triggered - - if not ctx.triggered: - return generate_graph(interval, data, params[1]) - - if ctx.triggered: - # Get most recently triggered id and prop_type - splitted = ctx.triggered[0]["prop_id"].split(".") - prop_id = splitted[0] - prop_type = splitted[1] - - if prop_type == "n_clicks": - curr_id = cur_fig["data"][0]["name"] - prop_id = prop_id[:-7] - if curr_id == prop_id: - return generate_graph(interval, data, curr_id) - else: - return generate_graph(interval, data, prop_id) - - if prop_type == "n_intervals" and cur_fig is not None: - curr_id = cur_fig["data"][0]["name"] - return generate_graph(interval, data, curr_id) - - -@callback( - Output("piechart", "figure"), - Input("interval-component", "n_intervals"), - State("value-setter-store", "data"), -) -def update_piechart(interval, stored_data): - if interval == 0: - return { - "data": [], - "layout": { - "font": {"color": "white"}, - "paper_bgcolor": "rgba(0,0,0,0)", - "plot_bgcolor": "rgba(0,0,0,0)", - }, - } - - if interval >= max_length: - total_count = max_length - 1 - else: - total_count = interval - 1 - - values = [] - colors = [] - for param in params[1:]: - ooc_param = (stored_data[param]["ooc"][total_count] * 100) + 1 - values.append(ooc_param) - if ooc_param > 6: - colors.append("#f45060") - else: - colors.append("#91dfd2") - - new_figure = { - "data": [ - { - "labels": params[1:], - "values": values, - "type": "pie", - "marker": {"colors": colors, "line": dict(color="white", width=2)}, - "hoverinfo": "label", - "textinfo": "label", - } - ], - "layout": { - "margin": dict(t=20, b=50), - "uirevision": True, - "font": {"color": "white"}, - "showlegend": False, - "paper_bgcolor": "rgba(0,0,0,0)", - "plot_bgcolor": "rgba(0,0,0,0)", - "autosize": True, - }, - } - return new_figure - - -if __name__ == "__main__": - app.run_server(debug=True) diff --git a/apps/dash-manufacture-spc-dashboard/utils/components.py b/apps/dash-manufacture-spc-dashboard/utils/components.py index 2c7287e29..b7d3b4fee 100644 --- a/apps/dash-manufacture-spc-dashboard/utils/components.py +++ b/apps/dash-manufacture-spc-dashboard/utils/components.py @@ -2,14 +2,9 @@ import dash_daq as daq import plotly.graph_objs as go -from app import params, max_length, state_dict, suffix_row, suffix_button_id, suffix_sparkline_graph, suffix_count, suffix_ooc_n, suffix_ooc_g, suffix_indicator +from constants import params, max_length, state_dict, suffix_row, suffix_button_id, suffix_sparkline_graph, suffix_count, suffix_ooc_n, suffix_ooc_g, suffix_indicator -ud_usl_input = daq.NumericInput(id="ud_usl_input", className="setting-input", size=200, max=9999999) -ud_lsl_input = daq.NumericInput(id="ud_lsl_input", className="setting-input", size=200, max=9999999) -ud_ucl_input = daq.NumericInput(id="ud_ucl_input", className="setting-input", size=200, max=9999999) -ud_lcl_input = daq.NumericInput(id="ud_lcl_input", className="setting-input", size=200, max=9999999) - def build_banner(): return html.Div( id="banner", @@ -25,15 +20,9 @@ def build_banner(): html.Div( id="banner-logo", children=[ + html.A("LEARN MORE", href="https://plotly.com/dash/", target="_blank", className="demo-button"), html.A( - html.Button(children="ENTERPRISE DEMO"), - href="https://plotly.com/get-demo/", - ), - html.Button( - id="learn-more-button", children="LEARN MORE", n_clicks=0 - ), - html.A( - html.Img(id="logo", src=get_asset_url("dash-logo-new.png")), + html.Img(id="logo", src=get_asset_url("images/plotly-logo.png")), href="https://plotly.com/dash/", ), ], @@ -113,25 +102,25 @@ def build_tab_1(): "value-setter-panel-usl", "Upper Specification limit", state_dict[params[1]]["usl"], - ud_usl_input, + daq.NumericInput(id="ud_usl_input", className="setting-input", size=200, max=9999999), ), build_value_setter_line( "value-setter-panel-lsl", "Lower Specification limit", state_dict[params[1]]["lsl"], - ud_lsl_input, + daq.NumericInput(id="ud_lsl_input", className="setting-input", size=200, max=9999999), ), build_value_setter_line( "value-setter-panel-ucl", "Upper Control limit", state_dict[params[1]]["ucl"], - ud_ucl_input, + daq.NumericInput(id="ud_ucl_input", className="setting-input", size=200, max=9999999), ), build_value_setter_line( "value-setter-panel-lcl", "Lower Control limit", state_dict[params[1]]["lcl"], - ud_lcl_input, + daq.NumericInput(id="ud_lcl_input", className="setting-input", size=200, max=9999999), ), ], ), diff --git a/apps/dash-manufacture-spc-dashboard/utils/graphs.py b/apps/dash-manufacture-spc-dashboard/utils/graphs.py index c261f4e4f..9e0c2881a 100644 --- a/apps/dash-manufacture-spc-dashboard/utils/graphs.py +++ b/apps/dash-manufacture-spc-dashboard/utils/graphs.py @@ -2,8 +2,7 @@ from dash import html, dcc from utils.components import generate_section_banner - -from app import params, state_dict, max_length +from constants import params, state_dict, max_length def generate_piechart(): return dcc.Graph(