%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#7F8C8D'}}}%%
flowchart LR
subgraph Sensors["Sensor Inputs"]
NTC[NTC Thermistor<br/>Temperature]
LDR[LDR<br/>Light Sensor]
POT[Potentiometer<br/>User Input]
end
subgraph ESP["ESP32"]
ADC1[ADC Channel 6<br/>GPIO 34]
ADC2[ADC Channel 7<br/>GPIO 35]
ADC3[ADC Channel 4<br/>GPIO 32]
WEB[Web Server<br/>Port 80]
end
subgraph Output["Dashboard"]
CHART1[Temperature<br/>Line Chart]
CHART2[Light Level<br/>Line Chart]
CHART3[Potentiometer<br/>Gauge]
end
NTC --> ADC1
LDR --> ADC2
POT --> ADC3
ADC1 --> WEB
ADC2 --> WEB
ADC3 --> WEB
WEB --> CHART1
WEB --> CHART2
WEB --> CHART3
style Sensors fill:#2C3E50,color:#fff
style ESP fill:#16A085,color:#fff
style Output fill:#E67E22,color:#fff
1282 Hands-On Labs: IoT Data Visualization
1282.1 Learning Objectives
By completing these hands-on labs, you will be able to:
- Configure ESP32 microcontrollers with multiple sensor inputs
- Create lightweight web servers on embedded hardware
- Implement real-time data visualization using Chart.js
- Build ASCII-art visualizations for Serial Monitor debugging
- Calculate and display running statistics (min, max, average)
- Implement threshold-based alerting for anomaly detection
- Apply visualization principles from sensors to browser display
Core Concept: These labs demonstrate the complete data flow from physical sensors through embedded processing to visual output - the same pattern used in production IoT systems at any scale.
Why It Matters: Understanding this end-to-end flow helps you debug problems (is the issue in sensing, processing, or display?), design efficient systems (where should processing happen?), and build prototypes that demonstrate real IoT concepts.
Key Takeaway: Start with Serial visualization for debugging (works everywhere, no network needed), graduate to web dashboards for user-facing displays. Both techniques remain useful throughout your IoT career.
1282.2 Lab Overview
This chapter contains three progressive labs:
| Lab | Focus | Duration | Hardware |
|---|---|---|---|
| Lab 1: Web Dashboard | Chart.js web visualization | 45 min | ESP32 + 3 sensors |
| Lab 2: Serial Visualization | ASCII bar charts & sparklines | 30 min | ESP32 + 2 sensors |
| Lab 3: Multi-Sensor Dashboard | Complete monitoring system | 45 min | ESP32 + 3 sensors |
All labs use the Wokwi online simulator - no physical hardware required.
1282.3 Lab 1: Real-Time Web Sensor Dashboard
1282.3.1 What Youβll Build
A complete real-time sensor dashboard running directly on an ESP32. The dashboard displays live temperature, light level, and user-controlled values with auto-updating charts - all served from the microcontroller itself with no external server required.
1282.3.2 Circuit Setup
| Component | ESP32 Pin | Purpose |
|---|---|---|
| Temperature Sensor (NTC) | GPIO 34 | Analog temperature reading |
| Light Sensor (LDR) | GPIO 35 | Ambient light level |
| Potentiometer | GPIO 32 | User-adjustable value |
1282.3.3 Wokwi Simulator
- Click inside the simulator below
- Delete the default code and paste the provided code
- Click the green βPlayβ button to start the simulation
- Click the generated URL in the serial monitor to open your dashboard
1282.3.4 Step 1: Configure the Circuit
Add this diagram.json configuration by clicking the diagram.json tab:
{
"version": 1,
"author": "IoT Class Lab",
"editor": "wokwi",
"parts": [
{ "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": 0, "left": 0 },
{ "type": "wokwi-ntc-temperature-sensor", "id": "ntc1", "top": -50, "left": 150 },
{ "type": "wokwi-photoresistor-sensor", "id": "ldr1", "top": -50, "left": 250 },
{ "type": "wokwi-potentiometer", "id": "pot1", "top": 150, "left": 200 }
],
"connections": [
["esp:GND.1", "ntc1:GND", "black", ["h0"]],
["esp:3V3", "ntc1:VCC", "red", ["h0"]],
["esp:34", "ntc1:OUT", "green", ["h0"]],
["esp:GND.1", "ldr1:GND", "black", ["h0"]],
["esp:3V3", "ldr1:VCC", "red", ["h0"]],
["esp:35", "ldr1:OUT", "orange", ["h0"]],
["esp:GND.1", "pot1:GND", "black", ["h0"]],
["esp:3V3", "pot1:VCC", "red", ["h0"]],
["esp:32", "pot1:SIG", "blue", ["h0"]]
]
}1282.3.5 Step 2: Complete Arduino Code
Copy this code into the Wokwi editor:
#include <WiFi.h>
#include <WebServer.h>
// Sensor pins
const int TEMP_PIN = 34; // NTC temperature sensor
const int LIGHT_PIN = 35; // LDR light sensor
const int POT_PIN = 32; // Potentiometer
// Web server on port 80
WebServer server(80);
// Sensor readings
float temperature = 0;
int lightLevel = 0;
int potValue = 0;
// HTML page with Chart.js visualization
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Sensor Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh; padding: 20px; color: #fff;
}
.header { text-align: center; margin-bottom: 30px; }
.header h1 { font-size: 2rem; color: #00d9ff; margin-bottom: 5px; }
.header p { color: #888; font-size: 0.9rem; }
.dashboard {
display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px; max-width: 1200px; margin: 0 auto;
}
.card {
background: rgba(255,255,255,0.05); border-radius: 16px;
padding: 20px; backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
}
.card h2 { font-size: 1rem; color: #888; margin-bottom: 10px; }
.value { font-size: 2.5rem; font-weight: bold; margin-bottom: 15px; }
.temp-value { color: #ff6b6b; }
.light-value { color: #ffd93d; }
.pot-value { color: #6bcb77; }
.chart-container { height: 150px; }
.status {
text-align: center; padding: 10px; margin-top: 20px;
background: rgba(0,217,255,0.1); border-radius: 8px;
font-size: 0.85rem; color: #00d9ff;
}
.gauge-container {
width: 200px; height: 100px; margin: 0 auto;
position: relative; overflow: hidden;
}
.gauge-bg {
width: 200px; height: 200px; border-radius: 50%;
background: conic-gradient(from 180deg, #2C3E50 0deg, #2C3E50 180deg, transparent 180deg);
position: absolute; top: 0;
}
.gauge-fill {
width: 200px; height: 200px; border-radius: 50%;
background: conic-gradient(from 180deg, #6bcb77 0deg, #6bcb77 var(--angle), transparent var(--angle));
position: absolute; top: 0; transition: all 0.3s ease;
}
.gauge-center {
width: 140px; height: 140px; border-radius: 50%;
background: #1a1a2e; position: absolute;
top: 30px; left: 30px;
display: flex; align-items: center; justify-content: center;
flex-direction: column;
}
</style>
</head>
<body>
<div class="header">
<h1>ESP32 Sensor Dashboard</h1>
<p>Real-time data visualization with Chart.js</p>
</div>
<div class="dashboard">
<div class="card">
<h2>TEMPERATURE</h2>
<div class="value temp-value"><span id="temp">--</span> C</div>
<div class="chart-container">
<canvas id="tempChart"></canvas>
</div>
</div>
<div class="card">
<h2>LIGHT LEVEL</h2>
<div class="value light-value"><span id="light">--</span>%</div>
<div class="chart-container">
<canvas id="lightChart"></canvas>
</div>
</div>
<div class="card">
<h2>POTENTIOMETER</h2>
<div class="value pot-value"><span id="pot">--</span>%</div>
<div class="gauge-container">
<div class="gauge-bg"></div>
<div class="gauge-fill" id="gaugeFill"></div>
<div class="gauge-center">
<span id="gaugeValue" style="font-size: 1.5rem; font-weight: bold;">0%</span>
</div>
</div>
</div>
</div>
<div class="status">
<span id="status">Connecting...</span> |
Updates: <span id="updateCount">0</span> |
Last: <span id="lastUpdate">--:--:--</span>
</div>
<script>
// Data arrays for charts (last 20 readings)
const maxPoints = 20;
let tempData = Array(maxPoints).fill(null);
let lightData = Array(maxPoints).fill(null);
let labels = Array(maxPoints).fill('');
let updateCount = 0;
// Chart.js configuration
const chartConfig = {
type: 'line',
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 300 },
plugins: { legend: { display: false } },
scales: {
x: { display: false },
y: {
grid: { color: 'rgba(255,255,255,0.1)' },
ticks: { color: '#888' }
}
},
elements: {
point: { radius: 0 },
line: { tension: 0.4 }
}
}
};
// Temperature chart
const tempChart = new Chart(document.getElementById('tempChart'), {
...chartConfig,
data: {
labels: labels,
datasets: [{
data: tempData,
borderColor: '#ff6b6b',
backgroundColor: 'rgba(255,107,107,0.1)',
fill: true
}]
}
});
// Light chart
const lightChart = new Chart(document.getElementById('lightChart'), {
...chartConfig,
data: {
labels: labels,
datasets: [{
data: lightData,
borderColor: '#ffd93d',
backgroundColor: 'rgba(255,217,61,0.1)',
fill: true
}]
}
});
// Fetch and update data
async function updateData() {
try {
const response = await fetch('/data');
const data = await response.json();
// Update values
document.getElementById('temp').textContent = data.temperature.toFixed(1);
document.getElementById('light').textContent = data.light;
document.getElementById('pot').textContent = data.potentiometer;
document.getElementById('gaugeValue').textContent = data.potentiometer + '%';
// Update gauge
const angle = (data.potentiometer / 100) * 180;
document.getElementById('gaugeFill').style.setProperty('--angle', angle + 'deg');
// Update charts
const time = new Date().toLocaleTimeString();
tempData.push(data.temperature);
tempData.shift();
lightData.push(data.light);
lightData.shift();
labels.push(time);
labels.shift();
tempChart.update('none');
lightChart.update('none');
// Update status
updateCount++;
document.getElementById('status').textContent = 'Connected';
document.getElementById('updateCount').textContent = updateCount;
document.getElementById('lastUpdate').textContent = time;
} catch (error) {
document.getElementById('status').textContent = 'Connection Error';
}
}
// Update every 500ms
setInterval(updateData, 500);
updateData();
</script>
</body>
</html>
)rawliteral";
void setup() {
Serial.begin(115200);
// Configure ADC pins
pinMode(TEMP_PIN, INPUT);
pinMode(LIGHT_PIN, INPUT);
pinMode(POT_PIN, INPUT);
// Connect to Wi-Fi (Wokwi virtual network)
Serial.println("\nConnecting to Wi-Fi...");
WiFi.begin("Wokwi-GUEST", "");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWi-Fi connected!");
Serial.print("Dashboard URL: http://");
Serial.println(WiFi.localIP());
// Setup web server routes
server.on("/", HTTP_GET, handleRoot);
server.on("/data", HTTP_GET, handleData);
server.begin();
Serial.println("Web server started!");
}
void loop() {
// Read sensors
int tempRaw = analogRead(TEMP_PIN);
int lightRaw = analogRead(LIGHT_PIN);
int potRaw = analogRead(POT_PIN);
// Convert to meaningful values
temperature = (tempRaw / 4095.0) * 100.0; // 0-100C range for simulation
// Light as percentage (0-100%)
lightLevel = map(lightRaw, 0, 4095, 0, 100);
// Potentiometer as percentage (0-100%)
potValue = map(potRaw, 0, 4095, 0, 100);
// Handle web requests
server.handleClient();
delay(10);
}
void handleRoot() {
server.send(200, "text/html", htmlPage);
}
void handleData() {
// Create JSON response
String json = "{";
json += "\"temperature\":" + String(temperature, 1) + ",";
json += "\"light\":" + String(lightLevel) + ",";
json += "\"potentiometer\":" + String(potValue);
json += "}";
server.send(200, "application/json", json);
}1282.3.6 Step 3: Running the Simulation
- Start the simulation: Click the green βPlayβ button
- Wait for Wi-Fi: Watch the Serial Monitor for βWi-Fi connected!β
- Open the dashboard: Click the IP address link (e.g.,
http://10.0.0.1) - Interact with sensors:
- Click on the NTC sensor and adjust temperature
- Click on the LDR and change light level
- Drag the potentiometer slider
- Dashboard doesnβt load? Check the Serial Monitor for the correct IP address
- Charts not updating? Ensure the simulation is running (green Play button)
- Compilation errors? Verify you copied the complete code including all brackets
1282.3.7 Key Concepts Demonstrated
| Concept | Implementation | Real-World Application |
|---|---|---|
| Real-time updates | 500ms polling interval | Balance between freshness and server load |
| Data decimation | Keep last 20 points | Prevent memory overflow on constrained devices |
| Visual hierarchy | Large values, smaller charts | Immediate status awareness, then trends |
| JSON API | Separate data endpoint | Clean separation of data and presentation |
| Responsive design | CSS Grid layout | Works on various screen sizes |
1282.4 Lab 2: Serial Data Visualization Dashboard
1282.4.1 What Youβll Build
A serial-based visualization dashboard that displays sensor data using ASCII graphics directly in the Serial Monitor. This approach works on any microcontroller without Wi-Fi capability and is invaluable for debugging.
1282.4.2 Why Serial Visualization?
| Aspect | Web Dashboard | Serial Visualization |
|---|---|---|
| Setup complexity | Requires Wi-Fi, HTML, JavaScript | Just Serial.print() |
| Debugging | Need browser, may miss fast events | Direct, real-time output |
| Hardware requirements | Wi-Fi-capable MCU | Any MCU with UART |
| Network dependency | Requires connection | Works offline |
| Best for | Production dashboards | Development, debugging |
1282.4.3 Circuit Setup
| Component | ESP32 Pin | Purpose |
|---|---|---|
| Potentiometer | GPIO 34 | User-adjustable value (0-100%) |
| Photoresistor (LDR) | GPIO 35 | Ambient light level |
1282.4.4 Wokwi Simulator
1282.4.5 Circuit Configuration
Add this diagram.json:
{
"version": 1,
"author": "IoT Class Lab",
"editor": "wokwi",
"parts": [
{ "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": 0, "left": 0 },
{ "type": "wokwi-potentiometer", "id": "pot1", "top": -80, "left": 150 },
{ "type": "wokwi-photoresistor-sensor", "id": "ldr1", "top": -80, "left": 280 }
],
"connections": [
["esp:GND.1", "pot1:GND", "black", ["h0"]],
["esp:3V3", "pot1:VCC", "red", ["h0"]],
["esp:34", "pot1:SIG", "green", ["h0"]],
["esp:GND.1", "ldr1:GND", "black", ["h0"]],
["esp:3V3", "ldr1:VCC", "red", ["h0"]],
["esp:35", "ldr1:OUT", "orange", ["h0"]]
]
}1282.4.6 Complete Arduino Code
// Serial Data Visualization Dashboard
// Demonstrates ASCII bar charts, sparklines, and real-time statistics
// Sensor pins
const int POT_PIN = 34; // Potentiometer
const int LIGHT_PIN = 35; // LDR light sensor
// History buffer for sparklines
const int HISTORY_SIZE = 40;
int potHistory[HISTORY_SIZE];
int lightHistory[HISTORY_SIZE];
int historyIndex = 0;
// Statistics tracking
float potSum = 0, lightSum = 0;
int potMin = 100, potMax = 0;
int lightMin = 100, lightMax = 0;
unsigned long sampleCount = 0;
// Timing control
unsigned long lastBarUpdate = 0;
unsigned long lastSparklineUpdate = 0;
unsigned long lastStatsUpdate = 0;
const int BAR_UPDATE_MS = 100; // Fast: 10 Hz
const int SPARKLINE_UPDATE_MS = 500; // Medium: 2 Hz
const int STATS_UPDATE_MS = 2000; // Slow: 0.5 Hz
// ASCII bar chart characters
const char BAR_FULL = '#';
const char BAR_EMPTY = '-';
const int BAR_WIDTH = 30;
// Sparkline characters (8 levels)
const char SPARK_CHARS[] = " _.-~*+#";
void setup() {
Serial.begin(115200);
delay(1000);
pinMode(POT_PIN, INPUT);
pinMode(LIGHT_PIN, INPUT);
for (int i = 0; i < HISTORY_SIZE; i++) {
potHistory[i] = 0;
lightHistory[i] = 0;
}
printHeader();
}
void printHeader() {
Serial.println("\n");
Serial.println("========================================================");
Serial.println(" SERIAL DATA VISUALIZATION DASHBOARD ");
Serial.println(" ESP32 Multi-Sensor Monitor ");
Serial.println("========================================================");
Serial.println();
Serial.println("Legend: Bars 10x/sec, Sparklines 2x/sec, Stats every 2 sec");
Serial.println(" Sparkline: _ . - ~ * + # (low to high)\n");
}
void drawBar(const char* label, int value, int maxVal) {
int filled = map(value, 0, maxVal, 0, BAR_WIDTH);
Serial.print(label);
Serial.print(" [");
for (int i = 0; i < BAR_WIDTH; i++) {
Serial.print(i < filled ? BAR_FULL : BAR_EMPTY);
}
Serial.print("] ");
if (value < 10) Serial.print(" ");
else if (value < 100) Serial.print(" ");
Serial.print(value);
Serial.println("%");
}
void drawSparkline(const char* label, int* history, int size) {
Serial.print(label);
Serial.print(" ");
for (int i = 0; i < size; i++) {
int idx = (historyIndex + i) % size;
int val = history[idx];
int charIdx = map(val, 0, 100, 0, 7);
charIdx = constrain(charIdx, 0, 7);
Serial.print(SPARK_CHARS[charIdx]);
}
Serial.println(" |");
}
void printStats(const char* label, int current, int minVal, int maxVal, float avg) {
Serial.print(" ");
Serial.print(label);
Serial.print(": Current=");
Serial.print(current);
Serial.print("% Min=");
Serial.print(minVal);
Serial.print("% Max=");
Serial.print(maxVal);
Serial.print("% Avg=");
Serial.print(avg, 1);
Serial.println("%");
}
void loop() {
unsigned long now = millis();
int potRaw = analogRead(POT_PIN);
int lightRaw = analogRead(LIGHT_PIN);
int potValue = map(potRaw, 0, 4095, 0, 100);
int lightValue = map(lightRaw, 0, 4095, 0, 100);
// Update statistics
sampleCount++;
potSum += potValue;
lightSum += lightValue;
if (potValue < potMin) potMin = potValue;
if (potValue > potMax) potMax = potValue;
if (lightValue < lightMin) lightMin = lightValue;
if (lightValue > lightMax) lightMax = lightValue;
// FAST UPDATE: Bar Charts (10 Hz)
if (now - lastBarUpdate >= BAR_UPDATE_MS) {
lastBarUpdate = now;
Serial.println("--- REAL-TIME BAR CHARTS ---");
drawBar("POT ", potValue, 100);
drawBar("LIGHT", lightValue, 100);
}
// MEDIUM UPDATE: Sparklines (2 Hz)
if (now - lastSparklineUpdate >= SPARKLINE_UPDATE_MS) {
lastSparklineUpdate = now;
potHistory[historyIndex] = potValue;
lightHistory[historyIndex] = lightValue;
historyIndex = (historyIndex + 1) % HISTORY_SIZE;
Serial.println("--- SPARKLINE HISTORY (20 sec) ---");
drawSparkline("POT ", potHistory, HISTORY_SIZE);
drawSparkline("LIGHT", lightHistory, HISTORY_SIZE);
}
// SLOW UPDATE: Statistics (0.5 Hz)
if (now - lastStatsUpdate >= STATS_UPDATE_MS) {
lastStatsUpdate = now;
float potAvg = potSum / sampleCount;
float lightAvg = lightSum / sampleCount;
Serial.println("=== STATISTICS SUMMARY ===");
printStats("POT ", potValue, potMin, potMax, potAvg);
printStats("LIGHT", lightValue, lightMin, lightMax, lightAvg);
Serial.print(" Samples: ");
Serial.print(sampleCount);
Serial.print(" Uptime: ");
Serial.print(now / 1000);
Serial.println(" sec\n");
}
delay(10);
}1282.4.7 Understanding the Output
Bar Charts (10 Hz): Show current values with immediate visual feedback
POT [###############---------------] 50%
LIGHT [########----------------------] 27%
Sparklines (2 Hz): Show 20-second history using ASCII characters
POT _.-~*+####+*~-._.-~*+# |
LIGHT ____....----....____.. |
The sparkline characters represent value ranges: - _ = 0-12% - . = 13-25% - - = 26-37% - ~ = 38-50% - * = 51-62% - + = 63-75% - # = 76-100%
1282.5 Lab 3: Multi-Sensor IoT Dashboard
1282.5.1 What Youβll Build
A comprehensive multi-sensor monitoring dashboard with temperature, light, and motion sensors. Features ASCII bar charts, sparklines, statistics, and threshold-based alerts.
1282.5.2 Circuit Setup
| Component | ESP32 Pin | Purpose |
|---|---|---|
| NTC Thermistor | GPIO 34 | Temperature sensing |
| Photoresistor (LDR) | GPIO 35 | Ambient light level |
| PIR Motion Sensor | GPIO 27 | Motion detection |
1282.5.3 Wokwi Simulator
1282.5.4 Circuit Configuration
{
"version": 1,
"author": "IoT Class Lab",
"editor": "wokwi",
"parts": [
{ "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": 0, "left": 0 },
{ "type": "wokwi-ntc-temperature-sensor", "id": "ntc1", "top": -80, "left": 120 },
{ "type": "wokwi-photoresistor-sensor", "id": "ldr1", "top": -80, "left": 220 },
{ "type": "wokwi-pir-motion-sensor", "id": "pir1", "top": -80, "left": 320 }
],
"connections": [
["esp:GND.1", "ntc1:GND", "black", ["h0"]],
["esp:3V3", "ntc1:VCC", "red", ["h0"]],
["esp:34", "ntc1:OUT", "green", ["h0"]],
["esp:GND.1", "ldr1:GND", "black", ["h0"]],
["esp:3V3", "ldr1:VCC", "red", ["h0"]],
["esp:35", "ldr1:OUT", "orange", ["h0"]],
["esp:GND.1", "pir1:GND", "black", ["h0"]],
["esp:3V3", "pir1:VCC", "red", ["h0"]],
["esp:27", "pir1:OUT", "purple", ["h0"]]
]
}1282.5.5 Key Features
This lab adds to the previous concepts:
- Threshold-based alerting: Warning and critical levels for temperature
- Motion event counting: Track discrete events, not just continuous values
- Alert panel: Display only when state changes (avoid alert fatigue)
- Multi-update rates: 10 Hz bars, 2 Hz sparklines, 0.5 Hz stats, 4 Hz alert checks
1282.5.6 Threshold Configuration
const float TEMP_WARNING = 30.0; // Temperature warning threshold (C)
const float TEMP_CRITICAL = 40.0; // Temperature critical threshold (C)
const int LIGHT_LOW = 20; // Low light warning (%)
const int LIGHT_HIGH = 90; // High light warning (%)The full code for this lab is available in the original chapter. Key additions:
checkAlerts()function that compares values to thresholdsprintAlertPanel()that displays alert status only on state changes- Motion event counting with
lastMotionTimetracking - Circular buffers for all sensor history
1282.6 Key Takeaways
These labs demonstrated fundamental IoT visualization principles:
Visual Encoding Hierarchy: Use position (bar length) for precise values, pattern (sparklines) for trends, and symbols/color only for critical alerts
Tiered Update Rates: Not all data needs the same refresh rate - match updates to human perception and data volatility
Context is Essential: Raw values without min/max/average context are nearly meaningless for decision-making
Alert-on-Change: Continuous alerting causes alarm fatigue - only notify when state changes
Memory Efficiency: Circular buffers provide trend history within fixed memory constraints
Information Density: Sparklines encode 50 data points in a single line - maximize information per screen space
- Grafana: Apply these same concepts using Grafana panels instead of ASCII art
- Dashboard Design: Use the 5-second rule - can you assess system status in 5 seconds?
- Production Systems: Add persistence (SD card logging) and network transmission
- Advanced Analytics: Implement moving averages, exponential smoothing, or FFT for pattern detection
1282.7 Whatβs Next
Apply your visualization skills to larger systems:
- Stream Processing: Process data streams before visualization
- Modeling and Inferencing: Add ML-based anomaly detection to your dashboards