1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-31 03:59:56 +02:00

Add setting to show measurement type on right Y-axis

This commit is contained in:
oliexdev
2025-08-13 13:53:32 +02:00
parent aa7a923946
commit 8691a33612
7 changed files with 129 additions and 32 deletions

View File

@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "d539026586245a45a135ae5bf3aaf73c",
"identityHash": "877ad250be34067d136497a388177415",
"entities": [
{
"tableName": "User",
@@ -214,7 +214,7 @@
},
{
"tableName": "MeasurementType",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `key` TEXT NOT NULL, `name` TEXT, `color` INTEGER NOT NULL, `icon` TEXT NOT NULL, `unit` TEXT NOT NULL, `inputType` TEXT NOT NULL, `displayOrder` INTEGER NOT NULL, `isDerived` INTEGER NOT NULL, `isEnabled` INTEGER NOT NULL, `isPinned` INTEGER NOT NULL)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `key` TEXT NOT NULL, `name` TEXT, `color` INTEGER NOT NULL, `icon` TEXT NOT NULL, `unit` TEXT NOT NULL, `inputType` TEXT NOT NULL, `displayOrder` INTEGER NOT NULL, `isDerived` INTEGER NOT NULL, `isEnabled` INTEGER NOT NULL, `isPinned` INTEGER NOT NULL, `isOnRightYAxis` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
@@ -280,6 +280,12 @@
"columnName": "isPinned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isOnRightYAxis",
"columnName": "isOnRightYAxis",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
@@ -292,7 +298,7 @@
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd539026586245a45a135ae5bf3aaf73c')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '877ad250be34067d136497a388177415')"
]
}
}

View File

@@ -59,7 +59,7 @@ import kotlinx.coroutines.launch
*/
fun getDefaultMeasurementTypes(): List<MeasurementType> {
return listOf(
MeasurementType(key = MeasurementTypeKey.WEIGHT, unit = UnitType.KG, color = 0xFFEF2929.toInt(), icon = "ic_weight", isPinned = true, isEnabled = true),
MeasurementType(key = MeasurementTypeKey.WEIGHT, unit = UnitType.KG, color = 0xFFEF2929.toInt(), icon = "ic_weight", isPinned = true, isEnabled = true, isOnRightYAxis = true),
MeasurementType(key = MeasurementTypeKey.BMI, color = 0xFFF57900.toInt(), icon = "ic_bmi", isDerived = true, isPinned = true, isEnabled = true),
MeasurementType(key = MeasurementTypeKey.BODY_FAT, color = 0xFFFFCE44.toInt(), icon = "ic_fat", isPinned = true, isEnabled = true),
MeasurementType(key = MeasurementTypeKey.WATER, color = 0xFF8AE234.toInt(), icon = "ic_water", isPinned = true, isEnabled = true),

View File

@@ -34,7 +34,8 @@ data class MeasurementType(
val displayOrder: Int = 0,
val isDerived: Boolean = false,
val isEnabled : Boolean = true,
val isPinned : Boolean = false
val isPinned : Boolean = false,
val isOnRightYAxis : Boolean = false
){
/**
* Gets the appropriate display name for UI purposes.

View File

@@ -18,6 +18,7 @@
package com.health.openscale.ui.screen.components
import android.text.Layout
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -66,6 +67,7 @@ import com.health.openscale.ui.screen.SharedViewModel
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottom
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberEnd
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStart
import com.patrykandpatrick.vico.compose.cartesian.layer.point
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
@@ -80,9 +82,12 @@ import com.patrykandpatrick.vico.compose.common.fill
import com.patrykandpatrick.vico.compose.common.insets
import com.patrykandpatrick.vico.compose.common.shape.markerCorneredShape
import com.patrykandpatrick.vico.core.cartesian.Zoom
import com.patrykandpatrick.vico.core.cartesian.axis.Axis
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis.ItemPlacer.Companion.count
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.CartesianLayerRangeProvider
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import com.patrykandpatrick.vico.core.cartesian.data.lineSeries
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
@@ -99,6 +104,8 @@ import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.math.ceil
import kotlin.math.floor
internal val DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("d MMM")
internal val X_TO_DATE_MAP_KEY = ExtraStore.Key<Map<Float, LocalDate>>() // Key for storing date mapping in chart model
@@ -390,34 +397,62 @@ fun LineChart(
return@Column // Exits the Column Composable early
}
// Determine colors for each series, using type's predefined color or gray as fallback.
val typeColors = remember(seriesEntries) {
seriesEntries.map { (type, _) -> // Index not used here
val seriesEntriesForStartAxis = remember(seriesEntries) {
seriesEntries.filter { (type, _) ->
!type.isOnRightYAxis
}
}
val typeColorsForStartAxis = remember(seriesEntriesForStartAxis) {
seriesEntriesForStartAxis.map { (type, _) ->
if (type.color != 0) Color(type.color) else Color.Gray
}
}
val seriesEntriesForEndAxis = remember(seriesEntries) {
seriesEntries.filter { (type, _) ->
type.isOnRightYAxis
}
}
val typeColorsForEndAxis = remember(seriesEntriesForEndAxis) {
seriesEntriesForEndAxis.map { (type, _) ->
if (type.color != 0) Color(type.color) else Color.Gray
}
}
val modelProducer = remember { CartesianChartModelProducer() }
// Update the chart model when series data or the date map changes.
LaunchedEffect(seriesEntries, xToDatesMapForStore) {
if (seriesEntries.isNotEmpty()) {
LaunchedEffect(seriesEntriesForStartAxis, seriesEntriesForEndAxis, xToDatesMapForStore) {
if (seriesEntriesForStartAxis.isNotEmpty() || seriesEntriesForEndAxis.isNotEmpty()) {
modelProducer.runTransaction {
lineSeries { // Vico's DSL for defining line series
seriesEntries.forEach { (_, sortedDateValuePairs) ->
val xValues = sortedDateValuePairs.map { it.first.toEpochDay().toFloat() }
val yValues = sortedDateValuePairs.map { it.second }
if (xValues.isNotEmpty()) {
series(x = xValues, y = yValues)
if (seriesEntriesForStartAxis.isNotEmpty()) {
lineSeries {
seriesEntriesForStartAxis.forEach { (_, sortedDateValuePairs) ->
val xValues = sortedDateValuePairs.map { it.first.toEpochDay().toFloat() }
val yValues = sortedDateValuePairs.map { it.second }
if (xValues.isNotEmpty()) {
series(x = xValues, y = yValues)
}
}
}
}
extras { it[X_TO_DATE_MAP_KEY] = xToDatesMapForStore } // Store date map in model extras
if (seriesEntriesForEndAxis.isNotEmpty()) {
lineSeries {
seriesEntriesForEndAxis.forEach { (_, sortedDateValuePairs) ->
val xValues = sortedDateValuePairs.map { it.first.toEpochDay().toFloat() }
val yValues = sortedDateValuePairs.map { it.second }
if (xValues.isNotEmpty()) {
series(x = xValues, y = yValues)
}
}
}
}
extras { it[X_TO_DATE_MAP_KEY] = xToDatesMapForStore }
}
} else {
// Clear the model if there are no series
modelProducer.runTransaction {
lineSeries {} // Empty series
lineSeries {}
lineSeries {}
extras { it.remove(X_TO_DATE_MAP_KEY) }
}
}
@@ -442,31 +477,75 @@ fun LineChart(
null // Hide X-axis when showing a single, targeted measurement type
}
val rangeProvider = remember {
object : CartesianLayerRangeProvider {
override fun getMinY(minY: Double, maxY: Double, extraStore: ExtraStore): Double {
val r = maxY - minY
return if (r == 0.0) minY - 1.0 else floor(minY - 0.1 * r)
}
override fun getMaxY(minY: Double, maxY: Double, extraStore: ExtraStore): Double {
val r = maxY - minY
return if (r == 0.0) maxY + 1.0 else ceil(maxY + 0.1 * r)
}
}
}
// Conditionally create Y-axis.
val yAxis = if (showYAxis) {
VerticalAxis.rememberStart(
valueFormatter = yAxisValueFormatter,
// guideline = rememberAxisGuidelineComponent(), // Optionally add Y-axis guidelines
val startYAxis = if (showYAxis) {
VerticalAxis.rememberStart(valueFormatter = yAxisValueFormatter)
} else { null }
val endYAxis = if (showYAxis) {
VerticalAxis.rememberEnd(valueFormatter = yAxisValueFormatter)
} else { null }
val lineProviderForStartAxis = remember(seriesEntriesForStartAxis, typeColorsForStartAxis) {
LineCartesianLayer.LineProvider.series(
seriesEntriesForStartAxis.mapIndexedNotNull { index, _ ->
if (index < typeColorsForStartAxis.size) {
createLineSpec(typeColorsForStartAxis[index], statisticsMode = targetMeasurementTypeId != null)
} else null
}
)
}
val lineLayerForStartAxis = if (seriesEntriesForStartAxis.isNotEmpty()) {
rememberLineCartesianLayer(
lineProvider = lineProviderForStartAxis,
verticalAxisPosition = Axis.Position.Vertical.Start,
rangeProvider = rangeProvider
)
} else {
null
}
// Define how lines are drawn (color, thickness, etc.)
val lineProvider = remember(seriesEntries, typeColors) {
val lineProviderForEndAxis = remember(seriesEntriesForEndAxis, typeColorsForEndAxis) {
LineCartesianLayer.LineProvider.series(
seriesEntries.mapIndexedNotNull { index, _ ->
if (index < typeColors.size) createLineSpec(typeColors[index], statisticsMode = targetMeasurementTypeId != null) else null
seriesEntriesForEndAxis.mapIndexedNotNull { index, _ ->
if (index < typeColorsForEndAxis.size) {
createLineSpec(typeColorsForEndAxis[index], statisticsMode = targetMeasurementTypeId != null)
} else null
}
)
}
val lineLayerForEndAxis = if (seriesEntriesForEndAxis.isNotEmpty()) {
rememberLineCartesianLayer(
lineProvider = lineProviderForEndAxis,
verticalAxisPosition = Axis.Position.Vertical.End,
rangeProvider = rangeProvider
)
} else {
null
}
val lineLayer = rememberLineCartesianLayer(lineProvider = lineProvider)
val layers : List<LineCartesianLayer> = remember(lineLayerForStartAxis, lineLayerForEndAxis) {
listOfNotNull(lineLayerForStartAxis, lineLayerForEndAxis)
}
val chart = rememberCartesianChart(
lineLayer,
startAxis = yAxis, // Y-axis
layers = layers.toTypedArray(),
startAxis = startYAxis, // left Y-axis
bottomAxis = xAxis, // X-axis
endAxis = endYAxis, // right Y-axis
marker = rememberMarker() // Interactive marker for data points
)

View File

@@ -110,6 +110,7 @@ fun MeasurementTypeDetailScreen(
var selectedIcon by remember { mutableStateOf(existingType?.icon ?: "ic_weight") } // Default icon
var isEnabled by remember { mutableStateOf(existingType?.isEnabled ?: true) } // Default to true for new types
var isPinned by remember { mutableStateOf(existingType?.isPinned ?: false) } // Default to false for new types
var isOnRightYAxis by remember { mutableStateOf(existingType?.isOnRightYAxis ?: false) }
var expandedUnit by remember { mutableStateOf(false) }
var expandedInputType by remember { mutableStateOf(false) }
@@ -138,7 +139,8 @@ fun MeasurementTypeDetailScreen(
isEnabled = isEnabled,
isPinned = isPinned,
key = existingType?.key ?: MeasurementTypeKey.CUSTOM, // New types are custom
isDerived = existingType?.isDerived ?: false // New types are not derived by default
isDerived = existingType?.isDerived ?: false, // New types are not derived by default
isOnRightYAxis = isOnRightYAxis
)
if (isEdit) {
@@ -311,6 +313,13 @@ fun MeasurementTypeDetailScreen(
onCheckedChange = { isPinned = it }
)
}
OutlinedSettingRow(label = stringResource(R.string.measurement_type_label_on_right_y_axis)) {
Switch(
checked = isOnRightYAxis,
onCheckedChange = { isOnRightYAxis = it }
)
}
}
// Color Picker Dialog

View File

@@ -141,6 +141,7 @@
<string name="measurement_type_label_unit">Einheit</string>
<string name="measurement_type_label_input_type">Eingabetyp</string>
<string name="measurement_type_label_pinned">Angeheftet</string>
<string name="measurement_type_label_on_right_y_axis">Auf rechter Y-Achse</string>
<string name="content_desc_selected_icon_preview">Vorschau des ausgewählten Symbols</string>
<string name="content_desc_drag_handle_sort">Sortieren</string>
<string name="content_desc_edit_type">Bearbeiten</string>

View File

@@ -142,6 +142,7 @@
<string name="measurement_type_label_unit">Unit</string>
<string name="measurement_type_label_input_type">Input Type</string>
<string name="measurement_type_label_pinned">Pinned</string>
<string name="measurement_type_label_on_right_y_axis">On right Y-axis</string>
<string name="content_desc_selected_icon_preview">Selected icon preview</string>
<string name="content_desc_drag_handle_sort">Sort</string>
<string name="content_desc_edit_type">Edit</string>