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 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 62fd54342..83fc67365 100644 --- a/apps/dash-manufacture-spc-dashboard/app.py +++ b/apps/dash-manufacture-spc-dashboard/app.py @@ -1,852 +1,22 @@ -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, html, dcc, Dash, Input, Output, State, callback, callback_context, dash_table import pandas as pd -app = dash.Dash( +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", + 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"))) - -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" - - -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(), + comp.build_banner(), dcc.Interval( id="interval-component", interval=2 * 1000, # in milliseconds @@ -856,50 +26,44 @@ def update_count(interval, col, data): html.Div( id="app-container", children=[ - build_tabs(), - # Main app - html.Div(id="app-content"), + comp.build_tabs(), + html.Div(id="app-content"), # Main app ], ), - dcc.Store(id="value-setter-store", data=init_value_setter_store()), + dcc.Store(id="value-setter-store", data=init_value_setter_store(df)), dcc.Store(id="n-interval-stage", data=50), - generate_modal(), + comp.generate_modal(), ], ) -@app.callback( - [Output("app-content", "children"), Output("interval-component", "n_intervals")], - [Input("app-tabs", "value")], - [State("n-interval-stage", "data")], +@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( + return comp.build_tab_1(), stopped_interval + return html.Div( id="status-container", children=[ - build_quick_stats_panel(), + comp.build_quick_stats_panel(), html.Div( id="graphs-container", - children=[build_top_panel(stopped_interval), build_chart_panel()], + children=[comp.build_top_panel(stopped_interval), build_chart_panel()], ), ], - ), - stopped_interval, - ) + ), stopped_interval -# Update interval -@app.callback( +@callback( Output("n-interval-stage", "data"), - [Input("app-tabs", "value")], - [ - State("interval-component", "n_intervals"), - State("interval-component", "disabled"), - State("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: @@ -910,25 +74,31 @@ def update_interval_state(tab_switch, cur_interval, disabled, cur_stage): 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")], +@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" -# ======= Callbacks for modal popup ======= -@app.callback( +@callback( Output("markdown", "style"), - [Input("learn-more-button", "n_clicks"), Input("markdown_close", "n_clicks")], + Input("learn-more-button", "n_clicks"), + Input("markdown_close", "n_clicks"), ) def update_click_output(button_click, close_click): - ctx = dash.callback_context + """ + Callbacks for modal popup + """ + ctx = callback_context if ctx.triggered: prop_id = ctx.triggered[0]["prop_id"].split(".")[0] @@ -938,12 +108,14 @@ def update_click_output(button_click, close_click): return {"display": "none"} -# ======= update progress gauge ========= -@app.callback( - output=Output("progress-gauge", "value"), - inputs=[Input("interval-component", "n_intervals")], +@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: @@ -952,93 +124,50 @@ def update_gauge(interval): 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")], +@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): - 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"), - ], + """ + 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): - if set_btn is None: - return data - else: + """ + 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 - # Recalculate ooc in case of param updates data[param]["ooc"] = populate_ooc(df[param], ucl, lcl) - return data + 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"), - ], +@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: @@ -1090,10 +219,8 @@ def show_current_specs(n_clicks, dd_select, store_data): # 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) + 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 @@ -1101,7 +228,7 @@ def callback(interval, stored_data): for param in params[1:]: update_param_row_function = create_callback(param) - app.callback( + callback( output=[ Output(param + suffix_count, "children"), Output(param + suffix_sparkline_graph, "extendData"), @@ -1114,24 +241,24 @@ def callback(interval, stored_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")], +@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): - # Find which one has been triggered - ctx = dash.callback_context + """ + 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]) @@ -1155,11 +282,10 @@ def update_control_chart(interval, n1, n2, n3, n4, n5, n6, n7, data, cur_fig): 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")], +@callback( + Output("piechart", "figure"), + Input("interval-component", "n_intervals"), + State("value-setter-store", "data"), ) def update_piechart(interval, stored_data): if interval == 0: @@ -1211,6 +337,5 @@ def update_piechart(interval, stored_data): return new_figure -# Running the server if __name__ == "__main__": - app.run_server(debug=True, port=8050) + app.run_server(debug=True) \ 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/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/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/assets/dash-logo-new.png b/apps/dash-manufacture-spc-dashboard/assets/dash-logo-new.png deleted file mode 100644 index 040cde174..000000000 Binary files a/apps/dash-manufacture-spc-dashboard/assets/dash-logo-new.png and /dev/null differ diff --git a/apps/dash-manufacture-spc-dashboard/img/screencapture1.png b/apps/dash-manufacture-spc-dashboard/assets/github/screencapture1.png similarity index 100% rename from apps/dash-manufacture-spc-dashboard/img/screencapture1.png rename to apps/dash-manufacture-spc-dashboard/assets/github/screencapture1.png diff --git a/apps/dash-manufacture-spc-dashboard/img/screencapture2.png b/apps/dash-manufacture-spc-dashboard/assets/github/screencapture2.png similarity index 100% rename from apps/dash-manufacture-spc-dashboard/img/screencapture2.png rename to apps/dash-manufacture-spc-dashboard/assets/github/screencapture2.png diff --git a/apps/dash-manufacture-spc-dashboard/assets/images/plotly-logo.png b/apps/dash-manufacture-spc-dashboard/assets/images/plotly-logo.png new file mode 100644 index 000000000..984dd57ab Binary files /dev/null and b/apps/dash-manufacture-spc-dashboard/assets/images/plotly-logo.png differ diff --git a/apps/dash-manufacture-spc-dashboard/assets/plotly_logo.png b/apps/dash-manufacture-spc-dashboard/assets/plotly_logo.png deleted file mode 100644 index 5a4dcb93e..000000000 Binary files a/apps/dash-manufacture-spc-dashboard/assets/plotly_logo.png and /dev/null differ 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/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..b7d3b4fee --- /dev/null +++ b/apps/dash-manufacture-spc-dashboard/utils/components.py @@ -0,0 +1,454 @@ +from dash import html, dcc, get_asset_url +import dash_daq as daq +import plotly.graph_objs as go + +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 + + +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("LEARN MORE", href="https://plotly.com/dash/", target="_blank", className="demo-button"), + html.A( + html.Img(id="logo", src=get_asset_url("images/plotly-logo.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"], + 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"], + 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"], + 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"], + daq.NumericInput(id="ud_lcl_input", className="setting-input", size=200, max=9999999), + ), + ], + ), + 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..9e0c2881a --- /dev/null +++ b/apps/dash-manufacture-spc-dashboard/utils/graphs.py @@ -0,0 +1,269 @@ +import plotly.graph_objs as go +from dash import html, dcc + +from utils.components import generate_section_banner +from constants 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 +