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:
parent
b9ee5ddfa3
commit
554b67e630
5 changed files with 27 additions and 37 deletions
|
|
@ -7,7 +7,7 @@ plugins {
|
|||
}
|
||||
|
||||
group = "dev.dtrentin"
|
||||
version = "0.4.1-SNAPSHOT"
|
||||
version = "0.5.0-SNAPSHOT"
|
||||
|
||||
kotlin {
|
||||
androidTarget {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
package dev.dtrentin.chart.model
|
||||
|
||||
enum class LodMode { MEAN, MIN_MAX, LTTB }
|
||||
enum class LodMode { MIN_MAX, LTTB }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue