EnvironmentMetrics chart scale improvements

This commit is contained in:
Jake-B 2025-03-12 17:45:40 -04:00
parent 95b006b442
commit 27ed32eb19
4 changed files with 84 additions and 12 deletions

View file

@ -17,7 +17,7 @@
2344A2B12D68DFF800170A77 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C49D8F2C471AEA0024FBD1 /* Constants.swift */; };
2373AE132D0A216C0086C749 /* MetricsChartSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE122D0A216C0086C749 /* MetricsChartSeries.swift */; };
2373AE152D0A24930086C749 /* MetricsSeriesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE142D0A24930086C749 /* MetricsSeriesList.swift */; };
2373AE172D0A26620086C749 /* EnviornmentDefaultSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE162D0A26620086C749 /* EnviornmentDefaultSeries.swift */; };
2373AE172D0A26620086C749 /* EnvironmentDefaultSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */; };
251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */; };
251926872C3BAE2200249DF5 /* NodeAlertsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */; };
2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */; };
@ -282,7 +282,7 @@
2344A2AE2D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TelemetryEntity+CoreDataProperties.swift"; sourceTree = "<group>"; };
2373AE122D0A216C0086C749 /* MetricsChartSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsChartSeries.swift; sourceTree = "<group>"; };
2373AE142D0A24930086C749 /* MetricsSeriesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsSeriesList.swift; sourceTree = "<group>"; };
2373AE162D0A26620086C749 /* EnviornmentDefaultSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnviornmentDefaultSeries.swift; sourceTree = "<group>"; };
2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultSeries.swift; sourceTree = "<group>"; };
251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteNodeButton.swift; sourceTree = "<group>"; };
251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAlertsButton.swift; sourceTree = "<group>"; };
251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExchangePositionsButton.swift; sourceTree = "<group>"; };
@ -600,7 +600,7 @@
isa = PBXGroup;
children = (
231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */,
2373AE162D0A26620086C749 /* EnviornmentDefaultSeries.swift */,
2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */,
231B3F262D0885240069A07D /* MetricsColumnDetail.swift */,
);
path = "Metrics Columns";
@ -1405,7 +1405,7 @@
DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */,
251926902C3CB44900249DF5 /* ClientHistoryButton.swift in Sources */,
DDD5BB102C285FB3007E03CA /* AppLogFilter.swift in Sources */,
2373AE172D0A26620086C749 /* EnviornmentDefaultSeries.swift in Sources */,
2373AE172D0A26620086C749 /* EnvironmentDefaultSeries.swift in Sources */,
DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */,
DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */,
DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */,

View file

@ -38,12 +38,18 @@ class MetricsChartSeries: ObservableObject {
// Possibly converted to the proper units
let valueClosure: (TelemetryEntity) -> Float?
// Used for scaling the Y-axis
let initialYAxisRange: ClosedRange<Float>?
let minumumYAxisSpan: Float?
// Main initializer
init<Value, ChartBody: ChartContent, ForegroundStyle: ShapeStyle>(
id: String,
keyPath: KeyPath<TelemetryEntity, Value>,
name: String,
abbreviatedName: String,
initialYAxisRange: ClosedRange<Float>? = nil,
minumumYAxisSpan: Float? = nil,
conversion: ((Value) -> Value)? = nil,
visible: Bool = true,
foregroundStyle: @escaping ((ClosedRange<Float>?) -> ForegroundStyle?) = { _ in nil },
@ -54,6 +60,8 @@ class MetricsChartSeries: ObservableObject {
self.id = id
self.name = name
self.abbreviatedName = abbreviatedName
self.initialYAxisRange = initialYAxisRange
self.minumumYAxisSpan = minumumYAxisSpan
self.visible = visible
// By saving these closures, MetricsChartSeries can be type agnostic

View file

@ -7,6 +7,7 @@
import Foundation
import SwiftUI
class MetricsSeriesList: ObservableObject, RandomAccessCollection, RangeReplaceableCollection {
@Published var series: [MetricsChartSeries]
@ -38,25 +39,86 @@ class MetricsSeriesList: ObservableObject, RandomAccessCollection, RangeReplacea
return nil
}
// Calculates the chartRange based on the series configuration and data provided
// Besides checkign the range of the data, this function also obeys some series-level
// configuraiton, such as:
// 1. starting with a desired fixed range
// 2. obeying a minimum span
func chartRange(forData data: [TelemetryEntity]) -> ClosedRange<Float> {
var lower: Float?
var upper: Float?
var globalLower: Float = .infinity
var globalUpper: Float = -.infinity
// Keep track of the range of each series
var range: [MetricsChartSeries: ClosedRange<Float>] = [:]
// Determine if there is an initial fixed range.
// The range might exapand past this initial range if the data goes beyond.
for aSeries in self.visible {
if let thisRange = aSeries.initialYAxisRange {
range[aSeries] = thisRange
if thisRange.upperBound > globalUpper {globalUpper = thisRange.upperBound}
if thisRange.lowerBound < globalLower {globalLower = thisRange.lowerBound}
}
}
// Iterate through all the data. It would be easier to iterate
// the series then the data, but this way we only iterate the data once
for te in data {
for aSeries in self.visible {
var seriesUpper = range[aSeries]?.upperBound ?? -.infinity
var seriesLower = range[aSeries]?.lowerBound ?? .infinity
if let value = aSeries.valueFor(te) {
if value > (upper ?? -.infinity) {upper = value}
if value < (lower ?? .infinity) {lower = value}
// Update the global bounds
if value > globalUpper {globalUpper = value}
if value < globalLower {globalLower = value}
// Update the series bounds if necessary
if value > seriesUpper || value < seriesLower {
if value > seriesUpper {
seriesUpper = value
}
if value < seriesLower {
seriesLower = value
}
if seriesUpper.isFinite && seriesLower.isFinite {
range[aSeries] = seriesLower...seriesUpper
}
}
}
}
}
// Return default range if no data or nil
guard let lower, let upper else {
// Go through each series one last time to obey the minimum span
for aSeries in self.visible {
if let minimumSpan = aSeries.minumumYAxisSpan,
let currentRange = range[aSeries] {
let currentSpan = currentRange.upperBound - currentRange.lowerBound
//Logger.data.info("Updated \(aSeries.id) to \(range[aSeries] ?? 0...0) span=\(currentSpan)")
if currentSpan < minimumSpan {
// Calculate the center of the range
let centerOfRange = currentRange.lowerBound + (currentSpan / 2)
let newLower = centerOfRange - (minimumSpan / 2.0)
let newUpper = centerOfRange + (minimumSpan / 2.0)
if newUpper > globalUpper {
globalUpper = newUpper
}
if newLower < globalLower {
globalLower = newLower
}
}
}
}
// Return default range if no data
if !globalLower.isFinite || !globalUpper.isFinite {
return 0.0...100.0
}
return lower...upper
return globalLower...globalUpper
}
// Collection conformance
typealias Index = Int
typealias Element = MetricsChartSeries

View file

@ -19,6 +19,7 @@ extension MetricsSeriesList {
keyPath: \.temperature,
name: "Temperature",
abbreviatedName: "Temp",
minumumYAxisSpan: 50.0,
conversion: { t in t.map { Float($0.localeTemperature()) } },
foregroundStyle: { chartRange in
let locale = NSLocale.current as NSLocale
@ -60,6 +61,7 @@ extension MetricsSeriesList {
keyPath: \.relativeHumidity,
name: "Relative Humidity",
abbreviatedName: "Hum",
initialYAxisRange: 0.0...100.0,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.purple.darker(componentDelta: 0.2)), .purple],