IoT Data Charts and Visualization (Temperature, Humidity, Pressure)
Displaying a temperature line for a week is simple. Problems start when there are 50,000 data points, user zooms to a 10-minute range, and the graph must still scroll smoothly on a budget Android device. Wrong library choice or naive implementation gives 3–4 FPS on scroll and OOM attempting to render everything at once.
Choosing a Chart Library
On Android — three real options:
| Library | Performance | Customization | Features |
|---|---|---|---|
MPAndroidChart |
Good up to ~5k points | Medium | Mature, XML+Compose wrapper |
Vico |
Excellent, Compose-first | Good | Native Compose, active development |
Charts (Compose) |
Good | Basic | Simple cases |
For IoT with large data volumes and zoom — Vico on Compose or MPAndroidChart with LineDataSet.setDrawCircles(false) + disabled setMode(CUBIC_BEZIER) (spline = expensive on large sets).
On iOS — Charts (MPAndroidChart fork for Swift) or DGCharts. Native Swift Charts (iOS 16+) — simple API, good performance, but limited customization.
Downsampling: Key to Performance
50,000 points over a month — a 400dp wide screen fits maximum 400 points. Displaying all 50,000 is wasted GPU work.
LTTB (Largest-Triangle-Three-Buckets) — data thinning algorithm preserving visual profile. On Android:
fun lttbDownsample(data: List<DataPoint>, threshold: Int): List<DataPoint> {
if (data.size <= threshold) return data
val sampled = mutableListOf<DataPoint>()
sampled.add(data.first())
val bucketSize = (data.size - 2).toDouble() / (threshold - 2)
var a = 0
for (i in 0 until threshold - 2) {
val bucketStart = ((i + 1) * bucketSize).toInt() + 1
val bucketEnd = minOf(((i + 2) * bucketSize).toInt() + 1, data.size - 1)
val nextA = bucketStart until bucketEnd
val avgX = nextA.sumOf { data[it].x } / nextA.count()
val avgY = nextA.sumOf { data[it].y } / nextA.count()
var maxArea = -1.0
var maxPoint = bucketStart
for (j in bucketStart until bucketEnd) {
val area = Math.abs(
(data[a].x - avgX) * (data[j].y - data[a].y) -
(data[a].x - data[j].x) * (avgY - data[a].y)
) * 0.5
if (area > maxArea) { maxArea = area; maxPoint = j }
}
sampled.add(data[maxPoint])
a = maxPoint
}
sampled.add(data.last())
return sampled
}
When zooming to short range — load original data from server for that period without downsampling. Range width < 1 hour → request data with original resolution.
Zoom and Scroll
Pinch-to-zoom gesture for chart — via ScaleGestureDetector (Android) or MagnificationGesture / onMagnification modifier. On range change — request new data from server.
"Infinite scroll" pattern for time series: when scrolling to loaded data edge — preload next period. Can't use Paging 3 directly (not for time series), but principle is same: prefetchDistance — load data when N units away from edge.
Multiple Parameters on One Chart
Temperature + humidity on one axis — bad: different units and ranges. Correct — two Y axes (MPAndroidChart supports via axisLeft / axisRight) or two separate synchronized charts with shared X axis.
With synchronized scroll of two charts — add OnChartGestureListener to each and programmatically scroll second when first scrolls:
chart1.onChartGestureListener = object : OnChartGestureListener {
override fun onChartTranslate(me: MotionEvent?, dX: Float, dY: Float) {
chart2.viewPortHandler.setTranslation(chart1.viewPortHandler.transX, 0f)
chart2.invalidate()
}
// other interface methods...
}
Chart Annotations and Events
Anomaly points, threshold lines, events (door open, device reboot) — important for analysis. LimitLine in MPAndroidChart for horizontal thresholds. Point annotations — custom MarkerView or VerticalHighlight with icon.
Color Ranges
For temperature: green (normal) → yellow (warning) → red (critical). LinearGradient on Y-axis in Compose Canvas or GradientColor in MPAndroidChart. Visually clear without legend where the problem period is.
Implementing IoT data charts with zoom, downsampling, and annotations: 3–5 weeks. Pricing depends on parameter count, data sources, and customization requirements.







