Реализация графиков и визуализации IoT-данных (температура, влажность, давление)
Отобразить линию температуры за неделю просто. Проблемы начинаются когда данных 50 000 точек, пользователь зумирует до диапазона в 10 минут, и при этом граф должен прокручиваться плавно на бюджетном Android-устройстве. Неправильный выбор библиотеки или наивная реализация даёт 3–4 FPS на скролл и OOM при попытке отрендерить всё сразу.
Выбор библиотеки для графиков
На Android — три реальных варианта:
| Библиотека | Производительность | Кастомизация | Особенности |
|---|---|---|---|
MPAndroidChart |
Хорошая до ~5к точек | Средняя | Mature, XML+Compose wrapper |
Vico |
Отличная, Compose-first | Хорошая | Нативный Compose, активная разработка |
Charts (Compose) |
Хорошая | Базовая | Простые кейсы |
Для IoT с большим объёмом данных и зумом — Vico на Compose или MPAndroidChart с LineDataSet.setDrawCircles(false) + setMode(CUBIC_BEZIER) выключенным (сплайн = дорого на больших наборах).
На iOS — Charts (fork MPAndroidChart для Swift) или DGCharts. Нативный Swift Charts (iOS 16+) — простой API, хорошая производительность, но ограниченная кастомизация.
Downsampling: ключ к производительности
50 000 точек за месяц — на экране шириной 400dp поместится максимум 400 точек. Отображать все 50 000 — пустая работа GPU.
LTTB (Largest-Triangle-Three-Buckets) — алгоритм прореживания данных с сохранением визуального профиля. На 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
}
При зуме до короткого диапазона — загружать исходные данные с сервера для этого периода без downsampling. Ширина диапазона < 1 часа → запрос данных с оригинальным разрешением.
Зум и скролл
Жест pinch-to-zoom для графика — через ScaleGestureDetector (Android) или MagnificationGesture / onMagnification modifier. При изменении диапазона — запрос новых данных с сервера.
Паттерн «бесконечный скролл» для временных серий: при скролле к краю загруженных данных — подгрузить следующий период. Paging 3 для этого использовать нельзя напрямую (он не для временных серий), но принцип тот же: prefetchDistance — загружать данные когда до края осталось N единиц времени.
Несколько параметров на одном графике
Температура + влажность на одной оси — плохо: разные единицы и диапазоны. Правильно — две оси Y (MPAndroidChart поддерживает через axisLeft / axisRight) или два отдельных синхронизированных графика с общей осью X.
При синхронизированном скролле двух графиков — добавить OnChartGestureListener к каждому и при скролле одного программно скроллить второй:
chart1.onChartGestureListener = object : OnChartGestureListener {
override fun onChartTranslate(me: MotionEvent?, dX: Float, dY: Float) {
chart2.viewPortHandler.setTranslation(chart1.viewPortHandler.transX, 0f)
chart2.invalidate()
}
// другие методы интерфейса...
}
Аннотации и события на графике
Точки аномалий, пороговые линии, события (открытие двери, перезагрузка устройства) — важны для анализа. LimitLine в MPAndroidChart для горизонтальных порогов. Точечные аннотации — кастомный MarkerView или VerticalHighlight с иконкой.
Цветовые диапазоны
Для температуры: зелёный (норма) → жёлтый (предупреждение) → красный (критично). LinearGradient по Y-оси в Compose Canvas или GradientColor в MPAndroidChart. Визуально сразу понятно без легенды, где проблемный период.
Реализация графиков IoT-данных с зумом, downsampling и аннотациями: 3–5 недель. Стоимость зависит от числа параметров, источников данных и требований к кастомизации.







