11import streamlit as st
22import pandas as pd
33import plotly .express as px
4+ import plotly .graph_objects as go
45from snowflake .snowpark .context import get_active_session
6+ import numpy as np
7+ from datetime import datetime , timedelta
58
69def load_machine_health_data (conn ):
710 """Load machine health data from Snowflake"""
@@ -68,9 +71,124 @@ def load_sensor_data(conn):
6871 st .error (f"Error loading sensor data: { str (e )} " )
6972 return pd .DataFrame ()
7073
74+ def create_gauge_chart (value , title , min_val , max_val , threshold_ranges ):
75+ """Create a gauge chart for sensor readings"""
76+ colors = ['#00ff00' , '#ffa500' , '#ff0000' ]
77+ fig = go .Figure (go .Indicator (
78+ mode = "gauge+number" ,
79+ value = value ,
80+ domain = {'x' : [0 , 1 ], 'y' : [0 , 1 ]},
81+ title = {'text' : title },
82+ gauge = {
83+ 'axis' : {'range' : [min_val , max_val ]},
84+ 'bar' : {'color' : "darkblue" },
85+ 'steps' : [
86+ {'range' : threshold_ranges [0 ], 'color' : colors [0 ]},
87+ {'range' : threshold_ranges [1 ], 'color' : colors [1 ]},
88+ {'range' : threshold_ranges [2 ], 'color' : colors [2 ]}
89+ ],
90+ 'threshold' : {
91+ 'line' : {'color' : "red" , 'width' : 4 },
92+ 'thickness' : 0.75 ,
93+ 'value' : threshold_ranges [1 ][1 ]
94+ }
95+ }
96+ ))
97+ fig .update_layout (height = 200 , margin = dict (l = 10 , r = 10 , t = 50 , b = 10 ))
98+ return fig
99+
100+ def create_time_series (df , machine_id , metric , anomaly_threshold = None ):
101+ """Create an interactive time series chart with anomaly detection"""
102+ machine_data = df [df ['machine_id' ] == machine_id ].copy ()
103+ machine_data ['timestamp' ] = pd .to_datetime (machine_data ['timestamp' ])
104+
105+ # Calculate rolling mean and std for anomaly detection
106+ machine_data ['rolling_mean' ] = machine_data [metric ].rolling (window = 20 ).mean ()
107+ machine_data ['rolling_std' ] = machine_data [metric ].rolling (window = 20 ).std ()
108+
109+ # Identify anomalies
110+ if anomaly_threshold :
111+ machine_data ['is_anomaly' ] = abs (machine_data [metric ] - machine_data ['rolling_mean' ]) > (anomaly_threshold * machine_data ['rolling_std' ])
112+
113+ # Create base line chart
114+ fig = go .Figure ()
115+
116+ # Add main metric line
117+ fig .add_trace (go .Scatter (
118+ x = machine_data ['timestamp' ],
119+ y = machine_data [metric ],
120+ name = metric .title (),
121+ mode = 'lines' ,
122+ line = dict (color = 'blue' ),
123+ hovertemplate =
124+ '<b>Time</b>: %{x}<br>' +
125+ '<b>Value</b>: %{y:.2f}<br>'
126+ ))
127+
128+ # Add anomaly points if detected
129+ if anomaly_threshold and machine_data ['is_anomaly' ].any ():
130+ anomalies = machine_data [machine_data ['is_anomaly' ]]
131+ fig .add_trace (go .Scatter (
132+ x = anomalies ['timestamp' ],
133+ y = anomalies [metric ],
134+ mode = 'markers' ,
135+ name = 'Anomalies' ,
136+ marker = dict (color = 'red' , size = 8 , symbol = 'circle' ),
137+ hovertemplate =
138+ '<b>Anomaly</b><br>' +
139+ '<b>Time</b>: %{x}<br>' +
140+ '<b>Value</b>: %{y:.2f}<br>'
141+ ))
142+
143+ # Update layout
144+ fig .update_layout (
145+ title = f"{ metric .title ()} Over Time - Machine { machine_id } " ,
146+ xaxis_title = "Time" ,
147+ yaxis_title = metric .title (),
148+ hovermode = 'x unified' ,
149+ showlegend = True ,
150+ height = 300 ,
151+ margin = dict (l = 10 , r = 10 , t = 50 , b = 10 )
152+ )
153+
154+ return fig
155+
71156# Page config
72157st .set_page_config (page_title = "Smart Factory Monitor" , layout = "wide" )
158+
159+ # Custom CSS
160+ st .markdown ("""
161+ <style>
162+ .stMetric {
163+ background-color: #1E2022;
164+ padding: 15px;
165+ border-radius: 8px;
166+ border: 1px solid #2E3236;
167+ min-height: 120px; /* Fixed height for all metric cards */
168+ }
169+ .stMetric:hover {
170+ background-color: #2E3236;
171+ border-color: #3E4246;
172+ }
173+ .stMetric [data-testid="stMetricLabel"] {
174+ color: #E0E2E6 !important;
175+ font-size: 1rem !important;
176+ }
177+ .stMetric [data-testid="stMetricValue"] {
178+ color: #FFFFFF !important;
179+ font-size: 2rem !important;
180+ }
181+ .stMetric [data-testid="stMetricDelta"] {
182+ color: #B0B2B6 !important;
183+ }
184+ .stProgress .st-bo {
185+ background-color: #00ff00;
186+ }
187+ </style>
188+ """ , unsafe_allow_html = True )
189+
73190st .title ("🏭 Smart Factory Health Monitor" )
191+ st .markdown ("Real-time monitoring and analytics dashboard for smart factory operations" )
74192
75193try :
76194 # Create Snowflake connection
@@ -98,11 +216,38 @@ def load_sensor_data(conn):
98216 st .warning ("No sensor data available." )
99217 st .stop ()
100218
219+ # Overview metrics
220+ st .subheader ("📊 Factory Overview" )
221+ overview_cols = st .columns (4 )
222+
223+ total_machines = len (health_data ['machine_id' ].unique ())
224+ healthy_machines = len (health_data [health_data ['health_status' ] == 'HEALTHY' ])
225+ critical_machines = len (health_data [health_data ['health_status' ] == 'CRITICAL' ])
226+
227+ overview_cols [0 ].metric ("Total Machines" , total_machines )
228+ overview_cols [1 ].metric (
229+ "Healthy Machines" ,
230+ healthy_machines ,
231+ delta = f"{ (healthy_machines / total_machines )* 100 :.1f} %" ,
232+ delta_color = "normal"
233+ )
234+ overview_cols [2 ].metric (
235+ "Critical Machines" ,
236+ critical_machines ,
237+ delta = f"{ (critical_machines / total_machines )* 100 :.1f} %" ,
238+ delta_color = "inverse"
239+ )
240+ overview_cols [3 ].metric (
241+ "Average Risk Score" ,
242+ f"{ health_data ['failure_risk_score' ].mean ():.2f} " ,
243+ delta_color = "normal"
244+ )
245+
101246 # Dashboard layout
102247 col1 , col2 = st .columns (2 )
103248
104249 with col1 :
105- st .subheader ("Machine Health Status" )
250+ st .subheader ("🔄 Machine Health Status" )
106251 status_counts = health_data ['health_status' ].value_counts ()
107252 fig = px .pie (values = status_counts .values ,
108253 names = status_counts .index ,
@@ -112,45 +257,163 @@ def load_sensor_data(conn):
112257 'NEEDS_MAINTENANCE' : '#ffa500' ,
113258 'CRITICAL' : '#ff0000'
114259 })
115- st .plotly_chart (fig )
260+ fig .update_traces (textposition = 'inside' , textinfo = 'percent+label' )
261+ st .plotly_chart (fig , use_container_width = True )
116262
117263 with col2 :
118- st .subheader ("Risk Scores by Machine " )
264+ st .subheader ("⚠️ Risk Analysis " )
119265 fig = px .bar (health_data ,
120266 x = 'machine_id' ,
121267 y = 'failure_risk_score' ,
122268 color = 'health_status' ,
123- title = "Failure Risk Scores" ,
269+ title = "Failure Risk Scores by Machine " ,
124270 color_discrete_map = {
125271 'HEALTHY' : '#00ff00' ,
126272 'NEEDS_MAINTENANCE' : '#ffa500' ,
127273 'CRITICAL' : '#ff0000'
128274 })
129- st .plotly_chart (fig )
275+ fig .update_layout (xaxis_title = "Machine ID" ,
276+ yaxis_title = "Risk Score" ,
277+ hovermode = 'x unified' )
278+ st .plotly_chart (fig , use_container_width = True )
130279
131280 # Detailed machine data
132- st .subheader ("Machine Details" )
133- for machine_id in health_data ['machine_id' ].unique ():
134- with st .expander (f"Machine { machine_id } " ):
135- machine_health = health_data [health_data ['machine_id' ] == machine_id ].iloc [0 ]
136- machine_sensors = sensor_data [sensor_data ['machine_id' ] == machine_id ].iloc [- 1 ]
137-
138- cols = st .columns (4 )
139- cols [0 ].metric ("Health Status" , machine_health ['health_status' ])
140- cols [1 ].metric ("Risk Score" , f"{ machine_health ['failure_risk_score' ]:.2f} " )
141- cols [2 ].metric ("Temperature" , f"{ machine_sensors ['temperature' ]:.1f} °C" )
142- cols [3 ].metric ("Vibration" , f"{ machine_sensors ['vibration' ]:.3f} " )
143-
144- st .info (f"Recommendation: { machine_health ['maintenance_recommendation' ]} " )
145-
146- # Show historical sensor data
147- machine_history = sensor_data [sensor_data ['machine_id' ] == machine_id ]
148- fig = px .line (machine_history ,
149- x = 'timestamp' ,
150- y = ['temperature' , 'vibration' , 'pressure' ],
151- title = f"Sensor History - Machine { machine_id } " )
152- st .plotly_chart (fig )
281+ st .subheader ("🔍 Machine Details" )
282+
283+ # Machine selector
284+ selected_machine = st .selectbox (
285+ "Select Machine for Detailed View" ,
286+ options = health_data ['machine_id' ].unique (),
287+ format_func = lambda x : f"Machine { x } "
288+ )
153289
290+ if selected_machine :
291+ machine_health = health_data [health_data ['machine_id' ] == selected_machine ].iloc [0 ]
292+ machine_sensors = sensor_data [sensor_data ['machine_id' ] == selected_machine ].iloc [- 1 ]
293+
294+ # Status indicators
295+ status_cols = st .columns (5 ) # Changed from 4 to 5 columns
296+
297+ # Health Status with color-coded delta
298+ status_color = {
299+ 'HEALTHY' : 'normal' ,
300+ 'NEEDS_MAINTENANCE' : 'inverse' ,
301+ 'CRITICAL' : 'inverse'
302+ }
303+
304+ # Determine risk level based on health status and risk score
305+ risk_score = float (machine_health ['failure_risk_score' ])
306+
307+ # Align risk assessment with health status
308+ if machine_health ['health_status' ] == 'HEALTHY' :
309+ risk_delta = "Low Risk"
310+ delta_color = 'normal'
311+ elif machine_health ['health_status' ] == 'NEEDS_MAINTENANCE' :
312+ risk_delta = "Medium Risk"
313+ delta_color = 'inverse'
314+ else : # CRITICAL
315+ risk_delta = "High Risk"
316+ delta_color = 'inverse'
317+
318+ status_cols [0 ].metric (
319+ "Health Status" ,
320+ machine_health ['health_status' ],
321+ delta = "Current Status" ,
322+ delta_color = status_color .get (machine_health ['health_status' ], 'normal' )
323+ )
324+
325+ status_cols [1 ].metric (
326+ "Risk Score" ,
327+ f"{ risk_score :.2f} " ,
328+ delta = risk_delta ,
329+ delta_color = delta_color
330+ )
331+
332+ # Calculate temperature delta and determine color
333+ temp_delta = machine_sensors ['temperature' ] - sensor_data [sensor_data ['machine_id' ] == selected_machine ]['temperature' ].mean ()
334+ temp_delta_color = 'inverse' if abs (temp_delta ) > 5 else 'normal'
335+
336+ status_cols [2 ].metric (
337+ "Temperature" ,
338+ f"{ machine_sensors ['temperature' ]:.1f} °C" ,
339+ delta = f"{ temp_delta :.1f} °C" ,
340+ delta_color = temp_delta_color
341+ )
342+
343+ # Calculate pressure delta and determine color
344+ pressure_delta = machine_sensors ['pressure' ] - sensor_data [sensor_data ['machine_id' ] == selected_machine ]['pressure' ].mean ()
345+ pressure_delta_color = 'inverse' if abs (pressure_delta ) > 10 else 'normal'
346+
347+ status_cols [3 ].metric (
348+ "Pressure" ,
349+ f"{ machine_sensors ['pressure' ]:.1f} " ,
350+ delta = f"{ pressure_delta :.1f} " ,
351+ delta_color = pressure_delta_color
352+ )
353+
354+ # Calculate vibration delta and determine color
355+ vib_delta = machine_sensors ['vibration' ] - sensor_data [sensor_data ['machine_id' ] == selected_machine ]['vibration' ].mean ()
356+ vib_delta_color = 'inverse' if abs (vib_delta ) > 0.1 else 'normal'
357+
358+ status_cols [4 ].metric (
359+ "Vibration" ,
360+ f"{ machine_sensors ['vibration' ]:.3f} " ,
361+ delta = f"{ vib_delta :.3f} " ,
362+ delta_color = vib_delta_color
363+ )
364+
365+ # Maintenance recommendation
366+ if machine_health ['maintenance_recommendation' ]:
367+ st .info (f"📋 Recommendation: { machine_health ['maintenance_recommendation' ]} " )
368+
369+ # Sensor gauges
370+ gauge_cols = st .columns (3 )
371+
372+ # Temperature gauge
373+ temp_ranges = [(0 , 50 ), (50 , 75 ), (75 , 100 )]
374+ temp_gauge = create_gauge_chart (
375+ machine_sensors ['temperature' ],
376+ "Temperature (°C)" ,
377+ 0 , 100 ,
378+ temp_ranges
379+ )
380+ gauge_cols [0 ].plotly_chart (temp_gauge , use_container_width = True )
381+
382+ # Pressure gauge
383+ pressure_ranges = [(0 , 100 ), (100 , 150 ), (150 , 200 )]
384+ pressure_gauge = create_gauge_chart (
385+ machine_sensors ['pressure' ],
386+ "Pressure" ,
387+ 0 , 200 ,
388+ pressure_ranges
389+ )
390+ gauge_cols [1 ].plotly_chart (pressure_gauge , use_container_width = True )
391+
392+ # Vibration gauge
393+ vibration_ranges = [(0 , 0.5 ), (0.5 , 0.8 ), (0.8 , 1.0 )]
394+ vibration_gauge = create_gauge_chart (
395+ machine_sensors ['vibration' ],
396+ "Vibration" ,
397+ 0 , 1 ,
398+ vibration_ranges
399+ )
400+ gauge_cols [2 ].plotly_chart (vibration_gauge , use_container_width = True )
401+
402+ # Time series charts with anomaly detection
403+ st .subheader ("📈 Sensor Trends" )
404+
405+ # Temperature time series
406+ temp_chart = create_time_series (sensor_data , selected_machine , 'temperature' , anomaly_threshold = 2 )
407+ st .plotly_chart (temp_chart , use_container_width = True )
408+
409+ # Vibration time series
410+ vib_chart = create_time_series (sensor_data , selected_machine , 'vibration' , anomaly_threshold = 2 )
411+ st .plotly_chart (vib_chart , use_container_width = True )
412+
413+ # Pressure time series
414+ pressure_chart = create_time_series (sensor_data , selected_machine , 'pressure' , anomaly_threshold = 2 )
415+ st .plotly_chart (pressure_chart , use_container_width = True )
416+
154417except Exception as e :
155418 st .error (f"Error in application: { str (e )} " )
156419 st .info ("Make sure LocalStack is running and the Snowflake emulator is properly configured." )
0 commit comments