Skip to content

Commit eea52a8

Browse files
authored
improve data visualization for the sensor data (#3)
* implement refreshed UI * fix issues * couple more fixes
1 parent 856a53f commit eea52a8

File tree

2 files changed

+291
-26
lines changed

2 files changed

+291
-26
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[server]
2+
runOnSave = true

app/src/module-ui/src/ui.py

Lines changed: 289 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import streamlit as st
22
import pandas as pd
33
import plotly.express as px
4+
import plotly.graph_objects as go
45
from snowflake.snowpark.context import get_active_session
6+
import numpy as np
7+
from datetime import datetime, timedelta
58

69
def 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
72157
st.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+
73190
st.title("🏭 Smart Factory Health Monitor")
191+
st.markdown("Real-time monitoring and analytics dashboard for smart factory operations")
74192

75193
try:
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+
154417
except 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

Comments
 (0)