1199 lines
38 KiB
Python
1199 lines
38 KiB
Python
import dash
|
|
from dash import dcc
|
|
from dash import html
|
|
from dash.dependencies import Input, Output, State
|
|
from dash.exceptions import PreventUpdate
|
|
from dash import dash_table
|
|
from dash.dash_table.Format import Group
|
|
import plotly.graph_objs as go
|
|
import dash_daq as daq
|
|
from textwrap import dedent
|
|
|
|
import pandas as pd
|
|
|
|
app = dash.Dash(__name__)
|
|
server = app.server
|
|
app.config['suppress_callback_exceptions'] = True
|
|
|
|
df = pd.read_csv("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'
|
|
|
|
theme = {
|
|
'dark': True,
|
|
'detail': '#2d3038', # Background-card
|
|
'primary': '#007439', # Green
|
|
'secondary': '#FFD15F', # Accent
|
|
}
|
|
|
|
|
|
def build_banner():
|
|
return html.Div(
|
|
id='banner',
|
|
className="banner",
|
|
children=[
|
|
html.H5('Manufacturing SPC Dashboard - Process Control and Exception Reporting'),
|
|
html.Button(
|
|
id='learn-more-button',
|
|
children="LEARN MORE",
|
|
n_clicks=0,
|
|
),
|
|
html.Img(
|
|
src="https://s3-us-west-1.amazonaws.com/plotly-tutorials/logo/new-branding/dash-logo-by-plotly-stripe-inverted.png")
|
|
]
|
|
)
|
|
|
|
|
|
def build_tabs():
|
|
return html.Div(
|
|
id='tabs',
|
|
className='row container scalable',
|
|
children=[
|
|
dcc.Tabs(
|
|
id='app-tabs',
|
|
value='tab1',
|
|
className='custom-tabs',
|
|
children=[
|
|
dcc.Tab(
|
|
id='Specs-tab',
|
|
label='Specification Settings',
|
|
value='tab1',
|
|
className='custom-tab',
|
|
selected_className='custom-tab--selected',
|
|
disabled_style={
|
|
'backgroundColor': '#2d3038',
|
|
'color': '#95969A',
|
|
'borderColor': '#23262E',
|
|
'display': 'flex',
|
|
'flex-direction': 'column',
|
|
'alignItems': 'center',
|
|
'justifyContent': 'center'
|
|
},
|
|
disabled=False
|
|
),
|
|
dcc.Tab(
|
|
id='Control-chart-tab',
|
|
label='Control Charts Dashboard',
|
|
value='tab2',
|
|
className='custom-tab',
|
|
selected_className='custom-tab--selected',
|
|
disabled_style= {
|
|
'backgroundColor': '#2d3038',
|
|
'color': '#95969A',
|
|
'borderColor': '#23262E',
|
|
'display': 'flex',
|
|
'flex-direction': 'column',
|
|
'alignItems': 'center',
|
|
'justifyContent': 'center'
|
|
},
|
|
disabled=False)
|
|
]
|
|
)
|
|
]
|
|
)
|
|
|
|
|
|
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(
|
|
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(
|
|
className='five columns',
|
|
children=[
|
|
html.Div(
|
|
id='value-setter-panel'),
|
|
html.Br(),
|
|
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', size=200, max=9999999, style={'width': '100%', 'height': '100%'})
|
|
ud_lsl_input = daq.NumericInput(id='ud_lsl_input', size=200, max=9999999, style={'width': '100%', 'height': '100%'})
|
|
ud_ucl_input = daq.NumericInput(id='ud_ucl_input', size=200, max=9999999, style={'width': '100%', 'height': '100%'})
|
|
ud_lcl_input = daq.NumericInput(id='ud_lcl_input', size=200, max=9999999, style={'width': '100%', 'height': '100%'})
|
|
|
|
|
|
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",
|
|
style={'display': 'none'},
|
|
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=dedent('''
|
|
**What is this mock app about?**
|
|
|
|
'dash-manufacture-spc-dashboard` is a dashboard for monitoring read-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.
|
|
'''))
|
|
)
|
|
]
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
app.layout = html.Div(
|
|
children=[
|
|
build_banner(),
|
|
build_tabs(),
|
|
# Main app
|
|
html.Div(
|
|
id='app-content',
|
|
className='container scalable'
|
|
),
|
|
html.Button('Proceed to Measurement', id='tab-trigger-btn', n_clicks=0,
|
|
style={'display': 'inline-block',
|
|
'float': 'right'}),
|
|
dcc.Store(
|
|
id='value-setter-store',
|
|
data=init_value_setter_store()
|
|
),
|
|
generate_modal(),
|
|
]
|
|
)
|
|
|
|
|
|
# ===== 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={
|
|
'backgroundColor': '#2d3038',
|
|
'fontWeight': 'bold'
|
|
},
|
|
style_as_list_view=True,
|
|
style_cell_conditional=[
|
|
{
|
|
'if': {'column_id': c},
|
|
'textAlign': 'left'
|
|
} for c in ['Specs']
|
|
],
|
|
style_cell={'backgroundColor': '#2d3038', 'color': '#95969A', 'border': '#53555B'},
|
|
data=new_df.to_dict('rows'),
|
|
columns=[{'id': c, 'name': c} for c in ['Specs', 'Current Setup']]
|
|
)
|
|
|
|
|
|
def build_quick_stats_panel():
|
|
return html.Div(
|
|
id="quick-stats",
|
|
className='row',
|
|
children=
|
|
[
|
|
html.Div(
|
|
id="card-1",
|
|
className='four columns',
|
|
children=[
|
|
html.H5("Operator ID"),
|
|
daq.LEDDisplay(
|
|
value='1704',
|
|
color=theme['secondary'],
|
|
size=50,
|
|
theme=theme
|
|
)
|
|
]
|
|
),
|
|
|
|
html.Div(
|
|
id='card-2',
|
|
className='four columns',
|
|
children=[
|
|
html.H5("Time to completion"),
|
|
daq.Gauge(
|
|
id='progress-gauge',
|
|
value=0,
|
|
size=150,
|
|
max=max_length * 2,
|
|
min=0,
|
|
color=theme['secondary']
|
|
)
|
|
]
|
|
),
|
|
|
|
html.Div(
|
|
id='utility-card',
|
|
className='four columns',
|
|
children=[
|
|
daq.StopButton(id='stop-button', size=160, buttonText='start')
|
|
]
|
|
)
|
|
]
|
|
)
|
|
|
|
|
|
def generate_section_banner(title):
|
|
return html.Div(
|
|
className="section-banner",
|
|
children=title,
|
|
)
|
|
|
|
|
|
def build_top_panel():
|
|
return html.Div(
|
|
id='top-section-container',
|
|
className='row',
|
|
style={
|
|
'height': '45vh'
|
|
},
|
|
children=[
|
|
# Metrics summary
|
|
html.Div(
|
|
id='metric-summary-session',
|
|
className='eight columns',
|
|
style={'height': '100%'},
|
|
children=[
|
|
generate_section_banner('Process Control Metrics Summary'),
|
|
generate_metric_list_header(),
|
|
html.Div(
|
|
# id='metric_div',
|
|
style={
|
|
'height': 'calc(100% - 90px)',
|
|
'width': '100%',
|
|
'overflow-x': 'hidden',
|
|
'overflow-y': 'scroll'
|
|
},
|
|
children=[
|
|
generate_metric_row_helper(1),
|
|
generate_metric_row_helper(2),
|
|
generate_metric_row_helper(3),
|
|
generate_metric_row_helper(4),
|
|
generate_metric_row_helper(5),
|
|
generate_metric_row_helper(6),
|
|
generate_metric_row_helper(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': params[1:],
|
|
'values': [1, 1, 1, 1, 1, 1, 1],
|
|
'type': 'pie',
|
|
'marker': {'line': {'color': '#53555B', 'width': 2}},
|
|
'hoverinfo': 'label',
|
|
'textinfo': 'label'
|
|
}],
|
|
'layout': {
|
|
'showlegend': True,
|
|
'paper_bgcolor': 'rgb(45, 48, 56)',
|
|
'plot_bgcolor': 'rgb(45, 48, 56)'
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
# Build header
|
|
def generate_metric_list_header():
|
|
return generate_metric_row(
|
|
'metric_header',
|
|
{
|
|
'height': '30px',
|
|
'margin': '10px 0px',
|
|
'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(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,
|
|
'children': html.Button(
|
|
id=button_id,
|
|
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': [], 'y': [], 'mode': 'lines+markers', 'name': item,
|
|
'line': {'color': 'rgb(255,209,95)'}}],
|
|
'layout': {
|
|
'uirevision': True,
|
|
'margin': dict(
|
|
l=0, r=0, t=4, b=4, pad=0
|
|
),
|
|
'paper_bgcolor': 'rgb(45, 48, 56)',
|
|
'plot_bgcolor': 'rgb(45, 48, 56)'
|
|
}
|
|
}))
|
|
},
|
|
{
|
|
'id': ooc_percentage_id,
|
|
'children': '0.00%'
|
|
},
|
|
{
|
|
'id': ooc_graph_id + '_container',
|
|
'children':
|
|
daq.GraduatedBar(
|
|
id=ooc_graph_id,
|
|
color={"gradient": True, "ranges": {"green": [0, 3], "yellow": [3, 7], "red": [7, 15]}},
|
|
showCurrentValue=False,
|
|
max=15,
|
|
value=0
|
|
)
|
|
},
|
|
{
|
|
'id': item + '_pf',
|
|
'children': daq.Indicator(
|
|
id=indicator_id,
|
|
value=True,
|
|
color=theme['primary']
|
|
)
|
|
}
|
|
)
|
|
|
|
|
|
def generate_metric_row(id, style, col1, col2, col3, col4, col5, col6):
|
|
if style is None:
|
|
style = {
|
|
'height': '100px',
|
|
'width': '100%',
|
|
}
|
|
return html.Div(
|
|
id=id,
|
|
className='row metric-row',
|
|
style=style,
|
|
children=[
|
|
html.Div(
|
|
id=col1['id'],
|
|
style={},
|
|
className='one column',
|
|
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%',
|
|
|
|
},
|
|
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.Interval(
|
|
id='interval-component',
|
|
interval=2 * 1000, # in milliseconds
|
|
n_intervals=0,
|
|
disabled=True
|
|
),
|
|
|
|
dcc.Store(
|
|
id='control-chart-state'
|
|
),
|
|
dcc.Graph(
|
|
id="control-chart-live",
|
|
figure=go.Figure({
|
|
'data': [{'x': [], 'y': [], 'mode': 'lines+markers', 'name': params[1]}],
|
|
'layout': {
|
|
'paper_bgcolor': 'rgb(45, 48, 56)',
|
|
'plot_bgcolor': 'rgb(45, 48, 56)'
|
|
}
|
|
}
|
|
)
|
|
)
|
|
]
|
|
)
|
|
|
|
|
|
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': 'rgb(255,209,95)'}
|
|
}
|
|
|
|
fig = {
|
|
'data': [
|
|
{
|
|
'x': x_array[:total_count],
|
|
'y': y_array[:total_count],
|
|
'mode': 'lines+markers',
|
|
'name': col,
|
|
'line': {'color': 'rgb(255,209,95)'}
|
|
},
|
|
ooc_trace,
|
|
histo_trace
|
|
]
|
|
}
|
|
|
|
len_figure = len(fig['data'][0]['x'])
|
|
|
|
fig['layout'] = dict(
|
|
hovermode='closest',
|
|
uirevision=col,
|
|
paper_bgcolor='rgb(45, 48, 56)',
|
|
plot_bgcolor='rgb(45, 48, 56)',
|
|
legend={'font': {'color': '#95969A'}},
|
|
font={'color': '#95969A'},
|
|
showlegend=True,
|
|
xaxis={
|
|
'zeroline': False,
|
|
'title': 'Batch_Num',
|
|
'showline': False,
|
|
'domain': [0, 0.8],
|
|
'titlefont': {'color': '#95969A'}
|
|
},
|
|
yaxis={
|
|
'title': col,
|
|
'autorange': True,
|
|
'titlefont': {'color': '#95969A'}
|
|
},
|
|
annotations=[
|
|
{'x': 0.75, 'y': lcl, 'xref': 'paper', 'yref': 'y',
|
|
'text': 'LCL:' + str(round(lcl, 3)),
|
|
'showarrow': False},
|
|
{'x': 0.75, 'y': ucl, 'xref': 'paper', 'yref': 'y',
|
|
'text': 'UCL: ' + str(round(ucl, 3)),
|
|
'showarrow': False},
|
|
{'x': 0.75, 'y': usl, 'xref': 'paper', 'yref': 'y',
|
|
'text': 'USL: ' + str(round(usl, 3)),
|
|
'showarrow': False},
|
|
{'x': 0.75, 'y': lsl, 'xref': 'paper', 'yref': 'y',
|
|
'text': 'LSL: ' + str(round(lsl, 3)),
|
|
'showarrow': False},
|
|
{'x': 0.75, 'y': mean, 'xref': 'paper', 'yref': 'y',
|
|
'text': 'Targeted mean: ' + str(round(mean, 3)),
|
|
'showarrow': False}
|
|
],
|
|
shapes=[
|
|
{
|
|
'type': 'line',
|
|
'xref': 'x',
|
|
'yref': 'y',
|
|
'x0': 1,
|
|
'y0': usl,
|
|
'x1': len_figure + 1,
|
|
'y1': usl,
|
|
'line': {
|
|
'color': 'rgb(50, 171, 96)',
|
|
'width': 1,
|
|
'dash': 'dashdot'
|
|
}
|
|
},
|
|
{
|
|
'type': 'line',
|
|
'xref': 'x',
|
|
'yref': 'y',
|
|
'x0': 1,
|
|
'y0': lsl,
|
|
'x1': len_figure + 1,
|
|
'y1': lsl,
|
|
'line': {
|
|
'color': 'rgb(50, 171, 96)',
|
|
'width': 1,
|
|
'dash': 'dashdot'
|
|
}
|
|
},
|
|
{
|
|
'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': 'dashdot'
|
|
}
|
|
},
|
|
{
|
|
'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': 'dashdot'
|
|
}
|
|
}
|
|
],
|
|
xaxis2={
|
|
'title': 'count',
|
|
'domain': [0.8, 1], # 70 to 100 % of width
|
|
'titlefont': {'color': '#95969A'}
|
|
},
|
|
yaxis2={
|
|
'anchor': 'free',
|
|
'overlaying': 'y',
|
|
'side': 'right',
|
|
'showticklabels': False,
|
|
'titlefont': {'color': '#95969A'}
|
|
}
|
|
)
|
|
|
|
return fig
|
|
|
|
|
|
@app.callback(
|
|
output=[Output('app-tabs', 'value'),
|
|
Output('app-content', 'children'),
|
|
Output('Specs-tab', 'disabled'),
|
|
Output('Control-chart-tab', 'disabled'),
|
|
Output('tab-trigger-btn', 'style')],
|
|
inputs=[Input('tab-trigger-btn', 'n_clicks')]
|
|
)
|
|
def render_tab_content(tab_switch):
|
|
if tab_switch == 0:
|
|
return 'tab1', build_tab_1(), False, True, {'display': 'inline-block', 'float': 'right'}
|
|
|
|
if tab_switch:
|
|
return ['tab2', daq.DarkThemeProvider(theme=theme, children=[
|
|
html.Div(
|
|
id='status-container',
|
|
children=[
|
|
build_quick_stats_panel(),
|
|
build_top_panel(),
|
|
build_chart_panel(),
|
|
]
|
|
)
|
|
]), True, False, {'display': 'none'}]
|
|
|
|
|
|
# ======= 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'}
|
|
|
|
|
|
# Callbacks for stopping interval update
|
|
@app.callback(
|
|
[Output('interval-component', 'disabled'),
|
|
Output('stop-button', 'buttonText')],
|
|
[Input('stop-button', 'n_clicks')],
|
|
state=[State('interval-component', 'disabled')]
|
|
)
|
|
def stop_production(_, current):
|
|
return not current, "stop" if current else "start"
|
|
|
|
|
|
# ======= 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)
|
|
|
|
|
|
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, theme['primary']
|
|
|
|
elif 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 = theme['primary']
|
|
else:
|
|
color = '#FF0000'
|
|
|
|
return str(total_count + 1), ooc_percentage_str, ooc_grad_val, color
|
|
|
|
|
|
# ======= update each row at interval =========
|
|
@app.callback(
|
|
output=[
|
|
Output(params[1] + suffix_count, 'children'),
|
|
Output(params[1] + suffix_sparkline_graph, 'extendData'),
|
|
Output(params[1] + suffix_ooc_n, 'children'),
|
|
Output(params[1] + suffix_ooc_g, 'value'),
|
|
Output(params[1] + suffix_indicator, 'color')
|
|
],
|
|
inputs=[Input('interval-component', 'n_intervals')],
|
|
state=[State('value-setter-store', 'data')]
|
|
)
|
|
def update_param1_row(interval, stored_data):
|
|
count, ooc_n, ooc_g_value, indicator = update_count(interval, params[1], stored_data)
|
|
spark_line_data = update_sparkline(interval, params[1])
|
|
return count, spark_line_data, ooc_n, ooc_g_value, indicator
|
|
|
|
|
|
@app.callback(
|
|
output=[
|
|
Output(params[2] + suffix_count, 'children'),
|
|
Output(params[2] + suffix_sparkline_graph, 'extendData'),
|
|
Output(params[2] + suffix_ooc_n, 'children'),
|
|
Output(params[2] + suffix_ooc_g, 'value'),
|
|
Output(params[2] + suffix_indicator, 'color')
|
|
],
|
|
inputs=[Input('interval-component', 'n_intervals')],
|
|
state=[State('value-setter-store', 'data')]
|
|
)
|
|
def update_param2_row(interval, stored_data):
|
|
count, ooc_n, ooc_g_value, indicator = update_count(interval, params[2], stored_data)
|
|
spark_line_data = update_sparkline(interval, params[2])
|
|
return count, spark_line_data, ooc_n, ooc_g_value, indicator
|
|
|
|
|
|
@app.callback(
|
|
output=[
|
|
Output(params[3] + suffix_count, 'children'),
|
|
Output(params[3] + suffix_sparkline_graph, 'extendData'),
|
|
Output(params[3] + suffix_ooc_n, 'children'),
|
|
Output(params[3] + suffix_ooc_g, 'value'),
|
|
Output(params[3] + suffix_indicator, 'color')
|
|
],
|
|
inputs=[Input('interval-component', 'n_intervals')],
|
|
state=[State('value-setter-store', 'data')]
|
|
)
|
|
def update_param3_row(interval, stored_data):
|
|
count, ooc_n, ooc_g_value, indicator = update_count(interval, params[3], stored_data)
|
|
spark_line_data = update_sparkline(interval, params[3])
|
|
return count, spark_line_data, ooc_n, ooc_g_value, indicator
|
|
|
|
|
|
@app.callback(
|
|
output=[
|
|
Output(params[4] + suffix_count, 'children'),
|
|
Output(params[4] + suffix_sparkline_graph, 'extendData'),
|
|
Output(params[4] + suffix_ooc_n, 'children'),
|
|
Output(params[4] + suffix_ooc_g, 'value'),
|
|
Output(params[4] + suffix_indicator, 'color')
|
|
],
|
|
inputs=[Input('interval-component', 'n_intervals')],
|
|
state=[State('value-setter-store', 'data')]
|
|
)
|
|
def update_param4_row(interval, stored_data):
|
|
count, ooc_n, ooc_g_value, indicator = update_count(interval, params[4], stored_data)
|
|
spark_line_data = update_sparkline(interval, params[4])
|
|
return count, spark_line_data, ooc_n, ooc_g_value, indicator
|
|
|
|
|
|
@app.callback(
|
|
output=[
|
|
Output(params[5] + suffix_count, 'children'),
|
|
Output(params[5] + suffix_sparkline_graph, 'extendData'),
|
|
Output(params[5] + suffix_ooc_n, 'children'),
|
|
Output(params[5] + suffix_ooc_g, 'value'),
|
|
Output(params[5] + suffix_indicator, 'color')
|
|
],
|
|
inputs=[Input('interval-component', 'n_intervals')],
|
|
state=[State('value-setter-store', 'data')]
|
|
)
|
|
def update_param5_row(interval, stored_data):
|
|
count, ooc_n, ooc_g_value, indicator = update_count(interval, params[5], stored_data)
|
|
spark_line_data = update_sparkline(interval, params[5])
|
|
return count, spark_line_data, ooc_n, ooc_g_value, indicator
|
|
|
|
|
|
@app.callback(
|
|
output=[
|
|
Output(params[6] + suffix_count, 'children'),
|
|
Output(params[6] + suffix_sparkline_graph, 'extendData'),
|
|
Output(params[6] + suffix_ooc_n, 'children'),
|
|
Output(params[6] + suffix_ooc_g, 'value'),
|
|
Output(params[6] + suffix_indicator, 'color')
|
|
],
|
|
inputs=[Input('interval-component', 'n_intervals')],
|
|
state=[State('value-setter-store', 'data')]
|
|
)
|
|
def update_param6_row(interval, stored_data):
|
|
count, ooc_n, ooc_g_value, indicator = update_count(interval, params[6], stored_data)
|
|
spark_line_data = update_sparkline(interval, params[6])
|
|
return count, spark_line_data, ooc_n, ooc_g_value, indicator
|
|
|
|
|
|
@app.callback(
|
|
output=[
|
|
Output(params[7] + suffix_count, 'children'),
|
|
Output(params[7] + suffix_sparkline_graph, 'extendData'),
|
|
Output(params[7] + suffix_ooc_n, 'children'),
|
|
Output(params[7] + suffix_ooc_g, 'value'),
|
|
Output(params[7] + suffix_indicator, 'color')
|
|
],
|
|
inputs=[Input('interval-component', 'n_intervals')],
|
|
state=[State('value-setter-store', 'data')]
|
|
)
|
|
def update_param7_row(interval, stored_data):
|
|
count, ooc_n, ooc_g_value, indicator = update_count(interval, params[7], stored_data)
|
|
spark_line_data = update_sparkline(interval, params[7])
|
|
return count, spark_line_data, ooc_n, ooc_g_value, indicator
|
|
|
|
|
|
# ======= 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': '#95969A'},
|
|
'paper_bgcolor': 'rgb(45, 48, 56)',
|
|
'plot_bgcolor': 'rgb(45, 48, 56)'}}
|
|
|
|
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('rgb(206,0,5)')
|
|
else:
|
|
colors.append('rgb(0, 116, 57)')
|
|
|
|
new_figure = {
|
|
'data': [
|
|
{
|
|
'labels': params[1:],
|
|
'values': values,
|
|
'type': 'pie',
|
|
'marker': {'colors': colors, 'line': dict(color='#53555B', width=2)},
|
|
'hoverinfo': 'label',
|
|
'textinfo': 'label'
|
|
}],
|
|
'layout': {
|
|
'uirevision': True,
|
|
'font': {'color': '#95969A'},
|
|
'showlegend': True,
|
|
'legend': {'font': {'color': '#95969A'}},
|
|
'paper_bgcolor': 'rgb(45, 48, 56)',
|
|
'plot_bgcolor': 'rgb(45, 48, 56)'
|
|
}
|
|
}
|
|
return new_figure
|
|
|
|
|
|
# Running the server
|
|
if __name__ == '__main__':
|
|
app.run_server(debug=True, port=8050)
|