refactor(buffer): MIN_MAX tier binning, remove MEAN lod mode

- TieredBuffer: replace MEAN binning with MIN_MAX (min+max per bin).
  Mathematically composable: MIN(bin_mins)=global_MIN, MAX(bin_maxes)=global_MAX.
  Eliminates tier-boundary visual artifacts. TIER1/TIER2 capacity doubled (2 samples/bin).
- LodMode: remove MEAN. Only MIN_MAX and LTTB remain.
  MEAN introduced values never present in raw signal — disinformation for sensor data.
- ChartConfig: default lodMode changed to MIN_MAX.
- Version: 0.4.1-SNAPSHOT → 0.5.0-SNAPSHOT

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dtrentin 2026-05-21 08:57:58 +02:00
parent b9ee5ddfa3
commit 554b67e630
5 changed files with 27 additions and 37 deletions

View file

@ -7,7 +7,7 @@ plugins {
}
group = "dev.dtrentin"
version = "0.4.1-SNAPSHOT"
version = "0.5.0-SNAPSHOT"
kotlin {
androidTarget {

View file

@ -44,25 +44,11 @@ class LodDecimator(
}
return when (mode) {
LodMode.MEAN -> decimateMean(n, pixelWidth, outX, outY)
LodMode.MIN_MAX -> decimateMinMax(n, pixelWidth, outX, outY)
LodMode.LTTB -> decimateLttb(n, pixelWidth, outX, outY)
}
}
private fun decimateMean(n: Int, pixelWidth: Int, outX: FloatArray, outY: FloatArray): Int {
for (col in 0 until pixelWidth) { bucketSumY[col] = 0.0; bucketCountArr[col] = 0 }
for (i in 0 until n) { bucketSumY[colArr[i]] += valArr[i]; bucketCountArr[colArr[i]]++ }
var outIdx = 0
for (col in 0 until pixelWidth) {
if (bucketCountArr[col] == 0) continue
outX[outIdx] = col + 0.5f
outY[outIdx] = (bucketSumY[col] / bucketCountArr[col]).toFloat()
outIdx++
}
return outIdx
}
private fun decimateMinMax(n: Int, pixelWidth: Int, outX: FloatArray, outY: FloatArray): Int {
for (col in 0 until pixelWidth) { colMin[col] = Float.MAX_VALUE; colMax[col] = -Float.MAX_VALUE; colHit[col] = false }
for (i in 0 until n) {

View file

@ -7,12 +7,12 @@ class TieredBuffer {
private val tier2 = CircularBuffer(TIER2_CAPACITY)
private var tier1BinStartMs = -1L
private var tier1BinSum = 0.0
private var tier1BinCount = 0
private var tier1BinMin = Float.MAX_VALUE
private var tier1BinMax = -Float.MAX_VALUE
private var tier2BinStartMs = -1L
private var tier2BinSum = 0.0
private var tier2BinCount = 0
private var tier2BinMin = Float.MAX_VALUE
private var tier2BinMax = -Float.MAX_VALUE
private val t0Ts = LongArray(TIER0_CAPACITY)
private val t0Vs = FloatArray(TIER0_CAPACITY)
@ -32,15 +32,17 @@ class TieredBuffer {
tier1BinStartMs = (ts / TIER1_BIN_MS) * TIER1_BIN_MS
}
if (ts >= tier1BinStartMs + TIER1_BIN_MS) {
if (tier1BinCount > 0) {
tier1.push(tier1BinStartMs + TIER1_BIN_MS / 2L, (tier1BinSum / tier1BinCount).toFloat())
if (tier1BinMin != Float.MAX_VALUE) {
val midTs = tier1BinStartMs + TIER1_BIN_MS / 2L
tier1.push(midTs, tier1BinMin)
tier1.push(midTs, tier1BinMax)
}
tier1BinStartMs = (ts / TIER1_BIN_MS) * TIER1_BIN_MS
tier1BinSum = value.toDouble()
tier1BinCount = 1
tier1BinMin = value
tier1BinMax = value
} else {
tier1BinSum += value
tier1BinCount++
if (value < tier1BinMin) tier1BinMin = value
if (value > tier1BinMax) tier1BinMax = value
}
}
@ -49,15 +51,17 @@ class TieredBuffer {
tier2BinStartMs = (ts / TIER2_BIN_MS) * TIER2_BIN_MS
}
if (ts >= tier2BinStartMs + TIER2_BIN_MS) {
if (tier2BinCount > 0) {
tier2.push(tier2BinStartMs + TIER2_BIN_MS / 2L, (tier2BinSum / tier2BinCount).toFloat())
if (tier2BinMin != Float.MAX_VALUE) {
val midTs = tier2BinStartMs + TIER2_BIN_MS / 2L
tier2.push(midTs, tier2BinMin)
tier2.push(midTs, tier2BinMax)
}
tier2BinStartMs = (ts / TIER2_BIN_MS) * TIER2_BIN_MS
tier2BinSum = value.toDouble()
tier2BinCount = 1
tier2BinMin = value
tier2BinMax = value
} else {
tier2BinSum += value
tier2BinCount++
if (value < tier2BinMin) tier2BinMin = value
if (value > tier2BinMax) tier2BinMax = value
}
}
@ -115,8 +119,8 @@ class TieredBuffer {
fun clear() {
tier0.clear(); tier1.clear(); tier2.clear()
tier1BinStartMs = -1L; tier1BinSum = 0.0; tier1BinCount = 0
tier2BinStartMs = -1L; tier2BinSum = 0.0; tier2BinCount = 0
tier1BinStartMs = -1L; tier1BinMin = Float.MAX_VALUE; tier1BinMax = -Float.MAX_VALUE
tier2BinStartMs = -1L; tier2BinMin = Float.MAX_VALUE; tier2BinMax = -Float.MAX_VALUE
}
companion object {
@ -132,8 +136,8 @@ class TieredBuffer {
const val TIER2_BIN_MS = 1000L / TIER2_HZ
val TIER0_CAPACITY = ((TIER0_DURATION_MS / 1000L) * TIER0_MAX_HZ).toInt()
val TIER1_CAPACITY = ((TIER1_DURATION_MS / 1000L) * TIER1_HZ).toInt()
val TIER2_CAPACITY = ((TIER2_DURATION_MS / 1000L) * TIER2_HZ).toInt()
val TIER1_CAPACITY = ((TIER1_DURATION_MS / 1000L) * TIER1_HZ).toInt() * 2
val TIER2_CAPACITY = ((TIER2_DURATION_MS / 1000L) * TIER2_HZ).toInt() * 2
val TOTAL_CAPACITY = TIER0_CAPACITY + TIER1_CAPACITY + TIER2_CAPACITY
}
}

View file

@ -22,5 +22,5 @@ data class ChartConfig(
val yLabelMode: AxisLabelMode = AxisLabelMode.INSIDE,
val yLabelDecimals: Int = 2,
val xLabelDecimals: Int = 1,
val lodMode: LodMode = LodMode.LTTB,
val lodMode: LodMode = LodMode.MIN_MAX,
)

View file

@ -1,3 +1,3 @@
package dev.dtrentin.chart.model
enum class LodMode { MEAN, MIN_MAX, LTTB }
enum class LodMode { MIN_MAX, LTTB }