Colorize headwind forecast line graph and the area beneath it (#155)
All checks were successful
Build / build (push) Successful in 5m19s
All checks were successful
Build / build (push) Successful in 5m19s
This commit is contained in:
parent
87a244ef27
commit
2378c944e6
@ -1,8 +1,10 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
import androidx.glance.GlanceModifier
|
import androidx.glance.GlanceModifier
|
||||||
import androidx.glance.Image
|
import androidx.glance.Image
|
||||||
import androidx.glance.ImageProvider
|
import androidx.glance.ImageProvider
|
||||||
@ -71,7 +73,8 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
|||||||
lineData: List<LineData>,
|
lineData: List<LineData>,
|
||||||
isImperial: Boolean,
|
isImperial: Boolean,
|
||||||
upcomingRoute: UpcomingRoute?,
|
upcomingRoute: UpcomingRoute?,
|
||||||
isPreview: Boolean
|
isPreview: Boolean,
|
||||||
|
context: Context
|
||||||
): Set<LineGraphBuilder.Line>
|
): Set<LineGraphBuilder.Line>
|
||||||
|
|
||||||
private fun previewFlow(settingsAndProfileStream: Flow<SettingsAndProfile>): Flow<StreamData> =
|
private fun previewFlow(settingsAndProfileStream: Flow<SettingsAndProfile>): Flow<StreamData> =
|
||||||
@ -265,8 +268,10 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
|||||||
data,
|
data,
|
||||||
settingsAndProfile.isImperialTemperature,
|
settingsAndProfile.isImperialTemperature,
|
||||||
upcomingRoute,
|
upcomingRoute,
|
||||||
config.preview
|
config.preview,
|
||||||
|
context
|
||||||
)
|
)
|
||||||
|
|
||||||
val bitmap = LineGraphBuilder(context).drawLineGraph(config.viewSize.first, config.viewSize.second, config.gridSize.first, config.gridSize.second, pointData) { x ->
|
val bitmap = LineGraphBuilder(context).drawLineGraph(config.viewSize.first, config.viewSize.second, config.gridSize.first, config.gridSize.second, pointData) { x ->
|
||||||
val startTime = data.firstOrNull()?.time
|
val startTime = data.firstOrNull()?.time
|
||||||
val time = startTime?.plus(floor(x).toLong(), ChronoUnit.HOURS)
|
val time = startTime?.plus(floor(x).toLong(), ChronoUnit.HOURS)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.UpcomingRoute
|
import de.timklge.karooheadwind.UpcomingRoute
|
||||||
import de.timklge.karooheadwind.screens.LineGraphBuilder
|
import de.timklge.karooheadwind.screens.LineGraphBuilder
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
@ -9,7 +10,8 @@ class PrecipitationForecastDataType(karooSystem: KarooSystemService) : LineGraph
|
|||||||
lineData: List<LineData>,
|
lineData: List<LineData>,
|
||||||
isImperial: Boolean,
|
isImperial: Boolean,
|
||||||
upcomingRoute: UpcomingRoute?,
|
upcomingRoute: UpcomingRoute?,
|
||||||
isPreview: Boolean
|
isPreview: Boolean,
|
||||||
|
context: Context
|
||||||
): Set<LineGraphBuilder.Line> {
|
): Set<LineGraphBuilder.Line> {
|
||||||
val precipitationPoints = lineData.map { data ->
|
val precipitationPoints = lineData.map { data ->
|
||||||
if (isImperial) { // Convert mm to inches
|
if (isImperial) { // Convert mm to inches
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import de.timklge.karooheadwind.HeadwindSettings
|
|||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.R
|
import de.timklge.karooheadwind.R
|
||||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
|
import de.timklge.karooheadwind.screens.isNightMode
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamDataFlow
|
import de.timklge.karooheadwind.streamDataFlow
|
||||||
import de.timklge.karooheadwind.streamDatatypeIsVisible
|
import de.timklge.karooheadwind.streamDatatypeIsVisible
|
||||||
@ -199,14 +200,10 @@ class TailwindAndRideSpeedDataType(
|
|||||||
"$sign${headwindSpeedUserUnit.roundToInt().absoluteValue} ${windSpeedUserUnit.roundToInt()}${gustSpeedAddon}"
|
"$sign${headwindSpeedUserUnit.roundToInt().absoluteValue} ${windSpeedUserUnit.roundToInt()}${gustSpeedAddon}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var dayColor = Color(ContextCompat.getColor(context, R.color.black))
|
|
||||||
var nightColor = Color(ContextCompat.getColor(context, R.color.white))
|
|
||||||
|
|
||||||
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
||||||
val windSpeedInKmh = headwindSpeed * 3.6
|
val windSpeedInKmh = headwindSpeed * 3.6
|
||||||
dayColor = interpolateWindColor(windSpeedInKmh, false, context)
|
val dayColor = interpolateWindColor(windSpeedInKmh, false, context)
|
||||||
nightColor = interpolateWindColor(windSpeedInKmh, true, context)
|
val nightColor = interpolateWindColor(windSpeedInKmh, true, context)
|
||||||
|
|
||||||
val result = glance.compose(context, DpSize.Unspecified) {
|
val result = glance.compose(context, DpSize.Unspecified) {
|
||||||
HeadwindDirection(
|
HeadwindDirection(
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.UpcomingRoute
|
import de.timklge.karooheadwind.UpcomingRoute
|
||||||
import de.timklge.karooheadwind.screens.LineGraphBuilder
|
import de.timklge.karooheadwind.screens.LineGraphBuilder
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
@ -9,7 +10,8 @@ class TemperatureForecastDataType(karooSystem: KarooSystemService) : LineGraphFo
|
|||||||
lineData: List<LineData>,
|
lineData: List<LineData>,
|
||||||
isImperial: Boolean,
|
isImperial: Boolean,
|
||||||
upcomingRoute: UpcomingRoute?,
|
upcomingRoute: UpcomingRoute?,
|
||||||
isPreview: Boolean
|
isPreview: Boolean,
|
||||||
|
context: Context
|
||||||
): Set<LineGraphBuilder.Line> {
|
): Set<LineGraphBuilder.Line> {
|
||||||
val linePoints = lineData.map { data ->
|
val linePoints = lineData.map { data ->
|
||||||
if (isImperial) {
|
if (isImperial) {
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import com.mapbox.turf.TurfConstants
|
import com.mapbox.turf.TurfConstants
|
||||||
import com.mapbox.turf.TurfMeasurement
|
import com.mapbox.turf.TurfMeasurement
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.UpcomingRoute
|
import de.timklge.karooheadwind.UpcomingRoute
|
||||||
import de.timklge.karooheadwind.lerpWeather
|
import de.timklge.karooheadwind.lerpWeather
|
||||||
import de.timklge.karooheadwind.screens.LineGraphBuilder
|
import de.timklge.karooheadwind.screens.LineGraphBuilder
|
||||||
|
import de.timklge.karooheadwind.screens.isNightMode
|
||||||
import de.timklge.karooheadwind.util.signedAngleDifference
|
import de.timklge.karooheadwind.util.signedAngleDifference
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
@ -23,7 +27,8 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD
|
|||||||
lineData: List<LineData>,
|
lineData: List<LineData>,
|
||||||
isImperial: Boolean,
|
isImperial: Boolean,
|
||||||
upcomingRoute: UpcomingRoute?,
|
upcomingRoute: UpcomingRoute?,
|
||||||
isPreview: Boolean
|
isPreview: Boolean,
|
||||||
|
context: Context
|
||||||
): Set<LineGraphBuilder.Line> {
|
): Set<LineGraphBuilder.Line> {
|
||||||
val windPoints = lineData.map { data ->
|
val windPoints = lineData.map { data ->
|
||||||
if (isImperial) { // Convert m/s to mph
|
if (isImperial) { // Convert m/s to mph
|
||||||
@ -47,16 +52,21 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD
|
|||||||
val t = i / HEADWIND_SAMPLE_COUNT.toDouble()
|
val t = i / HEADWIND_SAMPLE_COUNT.toDouble()
|
||||||
|
|
||||||
if (isPreview) {
|
if (isPreview) {
|
||||||
return@mapNotNull LineGraphBuilder.DataPoint(i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()), (-10f) + (20f * kotlin.random.Random.nextFloat()))
|
// Use a sine wave for headwind preview speed
|
||||||
|
val headwindSpeed = 10f * kotlin.math.sin(i * Math.PI * 2 / HEADWIND_SAMPLE_COUNT).toFloat()
|
||||||
|
val headwindSpeedInKmh = headwindSpeed * 3.6 // Convert m/s to km/h
|
||||||
|
|
||||||
|
return@mapNotNull LineGraphBuilder.DataPoint(x = i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()),
|
||||||
|
y = headwindSpeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (upcomingRoute == null) return@mapNotNull null
|
if (upcomingRoute == null) return@mapNotNull null
|
||||||
|
|
||||||
val beforeLineData = lineData.getOrNull(floor(lineData.size * t).toInt()) ?: lineData.firstOrNull()
|
val beforeLineData = lineData.getOrNull(floor((lineData.size) * t).toInt().coerceAtLeast(0)) ?: lineData.firstOrNull()
|
||||||
val afterLineData = lineData.getOrNull(ceil(lineData.size * t).toInt()) ?: lineData.lastOrNull()
|
val afterLineData = lineData.getOrNull(ceil((lineData.size) * t).toInt().coerceAtLeast(0)) ?: lineData.lastOrNull()
|
||||||
|
|
||||||
if (beforeLineData?.weatherData == null || afterLineData?.weatherData == null || beforeLineData.distance == null
|
if (beforeLineData?.weatherData == null || afterLineData?.weatherData == null || beforeLineData.distance == null
|
||||||
|| afterLineData.distance == null) return@mapNotNull null
|
|| afterLineData.distance == null || beforeLineData == afterLineData) return@mapNotNull null
|
||||||
|
|
||||||
val dt = remap(t.toFloat(),
|
val dt = remap(t.toFloat(),
|
||||||
floor(lineData.size * t).toFloat() / lineData.size,
|
floor(lineData.size * t).toFloat() / lineData.size,
|
||||||
@ -95,7 +105,10 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD
|
|||||||
headwindSpeed * 3.6 // Convert m/s to km/h
|
headwindSpeed * 3.6 // Convert m/s to km/h
|
||||||
}
|
}
|
||||||
|
|
||||||
LineGraphBuilder.DataPoint(i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()), headwindSpeedInUserUnit.toFloat())
|
LineGraphBuilder.DataPoint(
|
||||||
|
x = i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()),
|
||||||
|
y = headwindSpeedInUserUnit.toFloat()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
@ -106,34 +119,40 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD
|
|||||||
}
|
}
|
||||||
|
|
||||||
return buildSet {
|
return buildSet {
|
||||||
add(LineGraphBuilder.Line(
|
|
||||||
dataPoints = windPoints.mapIndexed { index, value ->
|
|
||||||
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
|
||||||
},
|
|
||||||
color = android.graphics.Color.GRAY,
|
|
||||||
label = "Wind" // if (!isImperial) "Wind km/h" else "Wind mph",
|
|
||||||
))
|
|
||||||
add(LineGraphBuilder.Line(
|
add(LineGraphBuilder.Line(
|
||||||
dataPoints = gustPoints.mapIndexed { index, value ->
|
dataPoints = gustPoints.mapIndexed { index, value ->
|
||||||
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
||||||
},
|
},
|
||||||
color = android.graphics.Color.DKGRAY,
|
color = Color.DKGRAY,
|
||||||
label = "Gust" // if (!isImperial) "Gust km/h" else "Gust mph",
|
label = "Gust" // if (!isImperial) "Gust km/h" else "Gust mph",
|
||||||
))
|
))
|
||||||
|
|
||||||
|
add(LineGraphBuilder.Line(
|
||||||
|
dataPoints = windPoints.mapIndexed { index, value ->
|
||||||
|
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
||||||
|
},
|
||||||
|
color = Color.GRAY,
|
||||||
|
label = "Wind" // if (!isImperial) "Wind km/h" else "Wind mph",
|
||||||
|
))
|
||||||
|
|
||||||
if (headwindPoints.isNotEmpty()) {
|
if (headwindPoints.isNotEmpty()) {
|
||||||
add(LineGraphBuilder.Line(
|
add(LineGraphBuilder.Line(
|
||||||
dataPoints = headwindPoints,
|
dataPoints = headwindPoints,
|
||||||
color = android.graphics.Color.MAGENTA,
|
color = if (isNightMode(context)) Color.WHITE else Color.BLACK,
|
||||||
label = "Headwind", // if (!isImperial) "Headwind km/h" else "Headwind mph",
|
label = "Head", // if (!isImperial) "Headwind km/h" else "Headwind mph",
|
||||||
drawCircles = false
|
drawCircles = false,
|
||||||
|
colorFunc = { headwindSpeed ->
|
||||||
|
val headwindSpeedInKmh = headwindSpeed * 3.6 // Convert m/s to km/h
|
||||||
|
interpolateWindColor(headwindSpeedInKmh, isNightMode(context), context).toArgb()
|
||||||
|
},
|
||||||
|
alpha = 255
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val HEADWIND_SAMPLE_COUNT = 30
|
const val HEADWIND_SAMPLE_COUNT = 50
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -9,30 +9,33 @@ import android.graphics.Paint
|
|||||||
import android.graphics.Paint.Align
|
import android.graphics.Paint.Align
|
||||||
import android.graphics.Path
|
import android.graphics.Path
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import kotlin.math.abs
|
|
||||||
import androidx.core.graphics.createBitmap
|
import androidx.core.graphics.createBitmap
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.floor
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
fun isNightMode(context: Context): Boolean {
|
||||||
|
val nightModeFlags = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||||
|
return nightModeFlags == Configuration.UI_MODE_NIGHT_YES
|
||||||
|
}
|
||||||
|
|
||||||
class LineGraphBuilder(val context: Context) {
|
class LineGraphBuilder(val context: Context) {
|
||||||
enum class YAxis {
|
enum class YAxis {
|
||||||
LEFT, RIGHT
|
LEFT, RIGHT
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DataPoint(val x: Float, val y: Float)
|
data class DataPoint(val x: Float, val y: Float) // color field removed
|
||||||
|
|
||||||
data class Line(
|
data class Line(
|
||||||
val dataPoints: List<DataPoint>,
|
val dataPoints: List<DataPoint>, // DataPoint type is now the new one
|
||||||
@ColorInt val color: Int,
|
@ColorInt val color: Int,
|
||||||
val label: String? = null,
|
val label: String? = null,
|
||||||
val yAxis: YAxis = YAxis.LEFT, // Default to left Y-axis
|
val yAxis: YAxis = YAxis.LEFT, // Default to left Y-axis
|
||||||
val drawCircles: Boolean = true // Default to true
|
val drawCircles: Boolean = true, // Default to true
|
||||||
|
val colorFunc: ((Float) -> Int)? = null, // Optional color function for dynamic colors,
|
||||||
|
val alpha: Int = 80
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun isNightMode(): Boolean {
|
|
||||||
val nightModeFlags = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
|
||||||
return nightModeFlags == Configuration.UI_MODE_NIGHT_YES
|
|
||||||
}
|
|
||||||
|
|
||||||
fun drawLineGraph(
|
fun drawLineGraph(
|
||||||
width: Int,
|
width: Int,
|
||||||
height: Int,
|
height: Int,
|
||||||
@ -41,10 +44,9 @@ class LineGraphBuilder(val context: Context) {
|
|||||||
lines: Set<Line>,
|
lines: Set<Line>,
|
||||||
labelProvider: ((Float) -> String)
|
labelProvider: ((Float) -> String)
|
||||||
): Bitmap {
|
): Bitmap {
|
||||||
val isNightMode = isNightMode()
|
|
||||||
|
|
||||||
val bitmap = createBitmap(width, height)
|
val bitmap = createBitmap(width, height)
|
||||||
val canvas = Canvas(bitmap)
|
val canvas = Canvas(bitmap)
|
||||||
|
val isNightMode = isNightMode(context)
|
||||||
|
|
||||||
val backgroundColor = if (isNightMode) Color.BLACK else Color.WHITE
|
val backgroundColor = if (isNightMode) Color.BLACK else Color.WHITE
|
||||||
val primaryTextColor = if (isNightMode) Color.WHITE else Color.BLACK
|
val primaryTextColor = if (isNightMode) Color.WHITE else Color.BLACK
|
||||||
@ -287,7 +289,7 @@ class LineGraphBuilder(val context: Context) {
|
|||||||
if (!hasRightYAxisData || abs(effectiveMaxYRight - effectiveMinYRight) < 0.0001f) 1f else (effectiveMaxYRight - effectiveMinYRight)
|
if (!hasRightYAxisData || abs(effectiveMaxYRight - effectiveMinYRight) < 0.0001f) 1f else (effectiveMaxYRight - effectiveMinYRight)
|
||||||
|
|
||||||
fun mapX(originalX: Float): Float {
|
fun mapX(originalX: Float): Float {
|
||||||
return graphLeft + ((originalX - effectiveMinX) / rangeX) * graphWidth
|
return floor(graphLeft + ((originalX - effectiveMinX) / rangeX) * graphWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mapYLeft(originalY: Float): Float {
|
fun mapYLeft(originalY: Float): Float {
|
||||||
@ -347,35 +349,113 @@ class LineGraphBuilder(val context: Context) {
|
|||||||
for (line in lines) {
|
for (line in lines) {
|
||||||
if (line.dataPoints.isEmpty()) continue
|
if (line.dataPoints.isEmpty()) continue
|
||||||
|
|
||||||
linePaint.color = line.color
|
|
||||||
val path = Path()
|
|
||||||
val firstPoint = line.dataPoints.first()
|
|
||||||
val mapY = if (line.yAxis == YAxis.LEFT) ::mapYLeft else ::mapYRight
|
val mapY = if (line.yAxis == YAxis.LEFT) ::mapYLeft else ::mapYRight
|
||||||
|
|
||||||
path.moveTo(mapX(firstPoint.x), mapY(firstPoint.y))
|
// Draw area between line and X axis, colorized per segment (match line colorization)
|
||||||
if (line.drawCircles) {
|
val zeroY = mapY((if (line.yAxis == YAxis.LEFT) effectiveMinYLeft else effectiveMinYRight).coerceAtLeast(0f))
|
||||||
canvas.drawCircle(
|
|
||||||
mapX(firstPoint.x),
|
|
||||||
mapY(firstPoint.y),
|
|
||||||
8f,
|
|
||||||
linePaint.apply { style = Paint.Style.FILL })
|
|
||||||
}
|
|
||||||
linePaint.style = Paint.Style.STROKE
|
|
||||||
|
|
||||||
for (i in 1 until line.dataPoints.size) {
|
for (i in 1 until line.dataPoints.size) {
|
||||||
val point = line.dataPoints[i]
|
val prev = line.dataPoints[i - 1]
|
||||||
path.lineTo(mapX(point.x), mapY(point.y))
|
val curr = line.dataPoints[i]
|
||||||
if (line.drawCircles) {
|
if (line.colorFunc != null) {
|
||||||
canvas.drawCircle(
|
val N = 4 // Number of sub-segments (tweak for smoothness/performance)
|
||||||
mapX(point.x),
|
val adjustment = 0.5f // Pixel adjustment to help close seams
|
||||||
mapY(point.y),
|
|
||||||
8f,
|
for (j in 0 until N) {
|
||||||
linePaint.apply { style = Paint.Style.FILL })
|
val t0 = j / N.toFloat()
|
||||||
|
val t1 = (j + 1) / N.toFloat()
|
||||||
|
val x0 = prev.x + (curr.x - prev.x) * t0
|
||||||
|
val y0 = prev.y + (curr.y - prev.y) * t0
|
||||||
|
val x1 = prev.x + (curr.x - prev.x) * t1
|
||||||
|
val y1 = prev.y + (curr.y - prev.y) * t1
|
||||||
|
val color0 = line.colorFunc.invoke(y0)
|
||||||
|
|
||||||
|
val mappedX0 = mapX(x0)
|
||||||
|
val mappedY0 = mapY(y0)
|
||||||
|
val mappedX1 = mapX(x1)
|
||||||
|
val mappedY1 = mapY(y1)
|
||||||
|
|
||||||
|
// Extend the right edge of internal sub-segments to create an overlap
|
||||||
|
val rightEdgeX = if (j < N - 1) mappedX1 + adjustment else mappedX1
|
||||||
|
|
||||||
|
val areaPath = Path().apply {
|
||||||
|
moveTo(mappedX0, mappedY0)
|
||||||
|
lineTo(rightEdgeX, mappedY1)
|
||||||
|
lineTo(rightEdgeX, zeroY)
|
||||||
|
lineTo(mappedX0, zeroY)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
val areaPaint = Paint().apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
color = color0
|
||||||
|
isAntiAlias = true
|
||||||
|
alpha = line.alpha
|
||||||
|
}
|
||||||
|
canvas.drawPath(areaPath, areaPaint)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val areaPath = Path().apply {
|
||||||
|
moveTo(mapX(prev.x), mapY(prev.y))
|
||||||
|
lineTo(mapX(curr.x), mapY(curr.y))
|
||||||
|
lineTo(mapX(curr.x), zeroY)
|
||||||
|
lineTo(mapX(prev.x), zeroY)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
val areaPaint = Paint().apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
color = line.color
|
||||||
|
isAntiAlias = true
|
||||||
|
alpha = line.alpha
|
||||||
|
}
|
||||||
|
canvas.drawPath(areaPath, areaPaint)
|
||||||
}
|
}
|
||||||
linePaint.style = Paint.Style.STROKE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.drawPath(path, linePaint)
|
// Draw the line, colorized per segment (improved: split for colorFunc)
|
||||||
|
for (i in 1 until line.dataPoints.size) {
|
||||||
|
val prev = line.dataPoints[i - 1]
|
||||||
|
val curr = line.dataPoints[i]
|
||||||
|
if (line.colorFunc != null) {
|
||||||
|
val N = 4 // Number of sub-segments (tweak for smoothness/performance)
|
||||||
|
for (j in 0 until N) {
|
||||||
|
val t0 = j / N.toFloat()
|
||||||
|
val t1 = (j + 1) / N.toFloat()
|
||||||
|
val x0 = prev.x + (curr.x - prev.x) * t0
|
||||||
|
val y0 = prev.y + (curr.y - prev.y) * t0
|
||||||
|
val x1 = prev.x + (curr.x - prev.x) * t1
|
||||||
|
val y1 = prev.y + (curr.y - prev.y) * t1
|
||||||
|
val color0 = line.colorFunc.invoke(y0)
|
||||||
|
// Optionally, blend color0 and color1 for the segment, or just use color0
|
||||||
|
val segPaint = Paint(linePaint).apply {
|
||||||
|
color = color0
|
||||||
|
}
|
||||||
|
canvas.drawLine(
|
||||||
|
mapX(x0), mapY(y0),
|
||||||
|
mapX(x1), mapY(y1),
|
||||||
|
segPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val segPaint = Paint(linePaint).apply {
|
||||||
|
color = line.color
|
||||||
|
}
|
||||||
|
canvas.drawLine(
|
||||||
|
mapX(prev.x), mapY(prev.y),
|
||||||
|
mapX(curr.x), mapY(curr.y),
|
||||||
|
segPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw circles if enabled
|
||||||
|
if (line.drawCircles) {
|
||||||
|
for (point in line.dataPoints) {
|
||||||
|
val circlePaint = Paint(linePaint).apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
color = line.colorFunc?.invoke(point.y) ?: line.color
|
||||||
|
}
|
||||||
|
canvas.drawCircle(mapX(point.x), mapY(point.y), 8f, circlePaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Left Y-axis ticks and labels
|
// Draw Left Y-axis ticks and labels
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user