Cloud & Data

IoT Data Visualization

Build live IoT dashboards with Grafana, Node-RED, and Python Streamlit — connect to InfluxDB, visualize sensor streams, and create alerting panels.

IoT Data Visualization

Raw numbers in a database are not useful until they are visualized. Good dashboards let operators spot trends, anomalies, and patterns at a glance. This topic covers Grafana (production dashboards), Node-RED (visual pipelines), and Python-based custom dashboards.


Grafana — Production Dashboards

Grafana is the industry-standard open-source dashboarding tool. It connects directly to InfluxDB, Prometheus, PostgreSQL, and dozens of other data sources.

Installation

bash
# Docker (fastest way to start)
docker run -d \
  --name=grafana \
  -p 3000:3000 \
  -v grafana-storage:/var/lib/grafana \
  grafana/grafana-oss

# Access: http://localhost:3000
# Default login: admin / admin

Connecting Grafana to InfluxDB

Grafana → Configuration → Data Sources → Add data source → InfluxDB

Settings:
  Query Language: Flux
  URL: http://localhost:8086
  Auth: Basic auth → ON
  Organization: bigxstar
  Token: your-influxdb-api-token
  Default Bucket: iot_sensors

Click "Save & Test" — should show "Data source connected"

Flux Queries for Panels

// Panel 1: Current temperature gauge
from(bucket: "iot_sensors")
  |> range(start: -5m)
  |> filter(fn: (r) => r._measurement == "environment")
  |> filter(fn: (r) => r._field == "temperature")
  |> filter(fn: (r) => r.device_id == "sensor-01")
  |> last()

// Panel 2: Temperature time-series (last 24 hours)
from(bucket: "iot_sensors")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "environment")
  |> filter(fn: (r) => r._field == "temperature")
  |> aggregateWindow(every: 5m, fn: mean, createEmpty: false)

// Panel 3: Min/Max/Avg stat panel
from(bucket: "iot_sensors")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "environment")
  |> filter(fn: (r) => r._field == "temperature")
  |> reduce(fn: (r, accumulator) => ({
      min: if r._value < accumulator.min then r._value else accumulator.min,
      max: if r._value > accumulator.max then r._value else accumulator.max,
      sum: accumulator.sum + r._value,
      count: accumulator.count + 1
    }), identity: {min: 9999.0, max: -9999.0, sum: 0.0, count: 0})

// Panel 4: Multi-device comparison
from(bucket: "iot_sensors")
  |> range(start: -6h)
  |> filter(fn: (r) => r._field == "temperature")
  |> group(columns: ["device_id"])
  |> aggregateWindow(every: 10m, fn: mean)

Grafana Alerting

json
// Alert rule configuration (via UI or API)
{
  "name": "High Temperature Alert",
  "condition": {
    "type": "threshold",
    "query": "last(temperature) WHERE device_id = 'sensor-01'",
    "threshold": {
      "above": 35.0
    },
    "for": "5m"
  },
  "notifications": [
    {
      "type": "email",
      "to": "admin@bigxstar.com",
      "subject": "High Temperature: {{ device_id }}"
    },
    {
      "type": "webhook",
      "url": "https://hooks.slack.com/your-webhook"
    }
  ]
}

Node-RED — Visual IoT Pipelines

Node-RED is a flow-based programming tool that connects IoT devices, cloud services, and databases through a drag-and-drop interface. It runs on Node.js and is pre-installed on Raspberry Pi OS.

bash
# Install Node-RED
npm install -g --unsafe-perm node-red

# Start
node-red

# Access: http://localhost:1880

Common Node-RED Flow

[MQTT in] → [JSON parse] → [Function] → [InfluxDB out]
                                     → [Dashboard gauge]
                                     → [Debug]

Example flow JSON (import via Node-RED menu):
json
[
  {
    "id": "mqtt_in",
    "type": "mqtt in",
    "broker": "mqtt-broker-config",
    "topic": "sensors/#",
    "qos": "1"
  },
  {
    "id": "json_parse",
    "type": "json",
    "action": "obj"
  },
  {
    "id": "validate",
    "type": "function",
    "func": "// Validate and enrich message\nif (!msg.payload.temperature) { return null; }\n\nmsg.payload.received_at = new Date().toISOString();\n\n// Route high temp to alert output\nif (msg.payload.temperature > 35) {\n    return [msg, msg];  // two outputs\n}\nreturn [msg, null];\n",
    "outputs": 2
  },
  {
    "id": "influx_out",
    "type": "influxdb out",
    "measurement": "environment"
  },
  {
    "id": "alert_email",
    "type": "e-mail",
    "to": "admin@example.com",
    "subject": "High Temperature Alert"
  }
]

Python Streamlit — Custom Dashboards in Minutes

Streamlit lets you build live-updating dashboards entirely in Python.

python
# pip install streamlit plotly influxdb-client pandas
# Run with: streamlit run dashboard.py

import streamlit as st
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
from influxdb_client import InfluxDBClient
from datetime import datetime
import time

# Page config
st.set_page_config(
    page_title="IoT Dashboard",
    page_icon="📡",
    layout="wide"
)

@st.cache_resource
def get_influx_client():
    return InfluxDBClient(
        url="http://localhost:8086",
        token="your-token",
        org="bigxstar"
    )

def query_sensor_data(device_id, hours=24):
    client = get_influx_client()
    query  = f'''
    from(bucket: "iot_sensors")
      |> range(start: -{hours}h)
      |> filter(fn: (r) => r._measurement == "environment")
      |> filter(fn: (r) => r.device_id == "{device_id}")
      |> filter(fn: (r) => r._field == "temperature" or r._field == "humidity")
      |> aggregateWindow(every: 5m, fn: mean, createEmpty: false)
      |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
    '''
    df = client.query_api().query_data_frame(query, org="bigxstar")
    if isinstance(df, list):
        df = pd.concat(df)
    return df

# Dashboard layout
st.title("🌡️ IoT Sensor Dashboard")
st.markdown(f"*Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")

# Sidebar controls
device_id = st.sidebar.selectbox("Device", ["sensor-01", "sensor-02", "sensor-03"])
time_range = st.sidebar.slider("Hours", 1, 72, 24)
auto_refresh = st.sidebar.toggle("Auto-refresh (30s)", value=True)

# Fetch data
df = query_sensor_data(device_id, hours=time_range)

if df.empty:
    st.warning("No data available for the selected period.")
else:
    # Metric cards
    col1, col2, col3, col4 = st.columns(4)
    with col1:
        latest_temp = df['temperature'].iloc[-1]
        st.metric("Current Temp", f"{latest_temp:.1f} °C",
                  delta=f"{latest_temp - df['temperature'].iloc[-2]:.1f}")
    with col2:
        latest_hum = df['humidity'].iloc[-1]
        st.metric("Current Humidity", f"{latest_hum:.1f} %")
    with col3:
        st.metric("Max Temp (period)", f"{df['temperature'].max():.1f} °C")
    with col4:
        st.metric("Min Temp (period)", f"{df['temperature'].min():.1f} °C")

    # Temperature chart
    fig_temp = px.line(df, x="_time", y="temperature",
                       title=f"Temperature — {device_id}",
                       labels={"_time": "Time", "temperature": "°C"})
    fig_temp.add_hline(y=35, line_dash="dash", line_color="red",
                       annotation_text="Alert threshold (35°C)")
    st.plotly_chart(fig_temp, use_container_width=True)

    # Temperature + Humidity dual axis
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=df['_time'], y=df['temperature'],
                             name="Temperature (°C)", yaxis="y1"))
    fig.add_trace(go.Scatter(x=df['_time'], y=df['humidity'],
                             name="Humidity (%)", yaxis="y2",
                             line=dict(color="orange")))
    fig.update_layout(
        title="Temperature & Humidity",
        yaxis=dict(title="Temperature (°C)"),
        yaxis2=dict(title="Humidity (%)", overlaying="y", side="right")
    )
    st.plotly_chart(fig, use_container_width=True)

    # Raw data table
    with st.expander("Raw Data"):
        st.dataframe(df[["_time", "temperature", "humidity"]].tail(50))

# Auto-refresh
if auto_refresh:
    time.sleep(30)
    st.rerun()

Flask + Chart.js — Lightweight Custom Dashboard

python
# pip install flask influxdb-client
from flask import Flask, render_template, jsonify
from influxdb_client import InfluxDBClient

app = Flask(__name__)
influx = InfluxDBClient(url="http://localhost:8086", token="token", org="bigxstar")

@app.route('/')
def dashboard():
    return render_template('dashboard.html')

@app.route('/api/readings/<device_id>')
def get_readings(device_id):
    query = f'''
    from(bucket: "iot_sensors")
      |> range(start: -1h)
      |> filter(fn: (r) => r.device_id == "{device_id}")
      |> filter(fn: (r) => r._field == "temperature")
      |> aggregateWindow(every: 1m, fn: last)
    '''
    tables = influx.query_api().query(query, org="bigxstar")
    data = []
    for table in tables:
        for record in table.records:
            data.append({
                "time": record.get_time().isoformat(),
                "value": round(record.get_value(), 2)
            })
    return jsonify(data)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)

Summary

  • Grafana is the go-to tool for production IoT dashboards — rich panels, alerting, and dozens of data source plugins
  • Node-RED enables visual flow-based IoT pipelines — connect MQTT to databases and dashboards without writing a single function
  • Streamlit lets Python developers build interactive dashboards in minutes using just Python code
  • Connect Grafana to InfluxDB using Flux queries for powerful time-series analysis
  • Configure Grafana alerts to notify via email, Slack, or webhook when thresholds are exceeded
  • Always enable auto-refresh on production dashboards and set appropriate refresh intervals (30s–5min)

Next, we'll explore edge computing — running intelligence on the device rather than the cloud.