英文:
Swift Charts LineChart: Display day on x-axis even if the respective γ-value is nill
问题
这是关于使用原生的Swift Charts框架时遇到的关于图表x轴的问题。
首先,让我告诉你我想要实现的图表行为:
我想要渲染一个图表,显示最近七天的体重数据(包括今天),类似于苹果健康应用程序在“周”选项卡中的显示方式。从苹果健康的截图中,你可以看到x轴始终显示过去七天,不管实际上是否有显示日的数据。在左侧的截图中,你可以看到整个星期,所有的天都有有效的数据。在右侧的截图中,你可以看到整个星期,但对于最后一天(“今天”),我删除了数据。苹果健康仍然正确显示了x轴上的日期,只是不使用LineChart的数据点,将x轴保持在固定的七天:
苹果健康应用程序中期望的x轴行为:
这是我尝试重新创建所描述的图表行为的方式:
每个图表数据元素都有一个.date
和可选的.averageWeight
:
struct WeightChartData: Identifiable
{
var id = UUID()
var date: Date
var averageWeight: Double?
}
我使用一个包含7个这样的WeightChartData
元素的数据数组,表示用户过去7天的体重数据(包括今天)。任何一天的体重数据都可能是nil
(当没有检索到HealthKit数据时)。该数组是用数据填充的,以便最后一个元素表示“今天”。这是我的测试/调试测试数组(这样你可以更好地看到我的实际数据数组的结构):
var testData: [WeightChartData] =
[
WeightChartData(date: Calendar.current.date(byAdding: .day, value: -6, to: Calendar.current.startOfDay(for: Date()))!, averageWeight: 95.1),
WeightChartData(date: Calendar.current.date(byAdding: .day, value: -5, to: Calendar.current.startOfDay(for: Date()))!, averageWeight: 94.2),
WeightChartData(date: Calendar.current.date(byAdding: .day, value: -4, to: Calendar.current.startOfDay(for: Date()))!, averageWeight: 94.4),
WeightChartData(date: Calendar.current.date(byAdding: .day, value: -3, to: Calendar.current.startOfDay(for: Date()))!, averageWeight: nil),
WeightChartData(date: Calendar.current.date(byAdding: .day, value: -2, to: Calendar.current.startOfDay(for: Date()))!, averageWeight: 93.5),
WeightChartData(date: Calendar.current.date(byAdding: .day, value: -1, to: Calendar.current.startOfDay(for: Date()))!, averageWeight: 93.6),
WeightChartData(date: Calendar.current.startOfDay(for: Date()), averageWeight: nil)
]
重要的是要注意,数组可能包括具有.averageWeight == nil
的WeightChartData
元素,这意味着这些天没有体重数据存在。
这是我尝试绘制图表的方式(WeightViewModel.Data
是我的“生产”数据数组,包含7个WeightChartData
元素)。请注意条件LineMark(...)
:
Chart(WeightViewModel.Data)
{ item in
if item.averageWeight != nil
{
LineMark(
x: .value("Date", item.date, unit: .weekday),
y: .value("Value", item.averageWeight!)
)
}
}
我使用这两个修饰符来确保我的y轴刻度围绕数据数组的最小和最大.averageWeight
。
.chartYScale(
domain: [ getBottomRange(), getTopRange() ]
)
.chartYAxis
{
AxisMarks(
position: .leading,
values: .automatic(desiredCount: getYMarkCount())
)
}
这个(γ轴自定义)按预期工作。但是,如果你认为这些函数可能引起了与x轴相关的问题,这里是getBottomRange()
、getTopRange()
和getYMarkCount()
:
func getTopRange() -> Int
{
return Int(round(WeightViewModel.maxWeightData()?.averageWeight ?? 0.0) + 1.0)
}
func getBottomRange() -> Int
{
return Int(round(WeightViewModel.minWeightData()?.averageWeight ?? 0.0) - 1.0)
}
func getYMarkCount() -> Int
{
return abs(getTopRange() - getBottomRange())
}
对于x轴,我使用以下方法创建一个AxisMark(...)
,每天一个(所以x轴上有7个元素,不管元素包含体重数据还是nil,就像在苹果健康中一样):
.chartXAxis
{
AxisMarks(values: .stride(by: .day))
{ date in
AxisGridLine()
AxisValueLabel(format: .dateTime.weekday(.narrow), centered: true)
}
}
现在让我展示一下如果有一天的.averageWeight == nil
会发生什么:
- 如果WeightViewModel.Data的所有元素都有有效的
.averageWeight
:图形绘制正确(表示x轴上的所有7天):
- 如果一个元素(不是第一个或最后一个元素)的
.averageWeight == nil
,则图表会正确地跳过此y值,同时仍然显示x轴上的日期:
就像苹果健康一样。在这个示例中,只有倒数第二个元素的.averageWeight == nil
。
- 问题是当第一个或最后一个元素的
.averageWeight == nil
时。再次,因为LineMark(..)
只在item.averageWeight != nil
时调用,所以会跳过这个第一个或最后一个数据点。但这一次,因为它是第一个或最后一个元素,我的x轴刻
英文:
It is my first time using the native Swift Charts framework and I ran into a problem regarding my x-axis of my chart.
First, let me tell you about the chart behaviour I want to achieve:
I want to render a chart that displays weight data of the last seven days (including today), similarly to how the Apple Health app does this in the "week-tab". From the screenshot of Apple Health you can see that the x-Axis always has the last seven days on it, irrespective whether there actually is data for the displayed days. In the left screenshot you see a full week in which all days have valid data. In the right screenshot you see a full week but for the last day ("today") I removed the data. Apple Health still rightly shows the day on the x-axis, it just doesn't use the data point for the LineChart, keeping the x-axis to a fixed 7 days:
Desired x-axis behaviour in Apple Health app:
Here is my attempt to re-create the described chart behavior:
Every chart data element has a .date
and optinally an .averageWeight
:
struct WeightChartData: Identifiable
{
var id = UUID()
var date: Date
var averageWeight: Double?
}
I use a data array containing 7 such WeightChartData
elements, representing the users last 7 days of weight data (including today). Weight data on any day might be nill
(when no HealthKit data was retrieved for that day). The array is populated with data so that the last element represents "today". Here is my testing/debugging test array (so you can better see the structure of my actual data array):
var testData: [WeightChartData] =
[
WeightChartData(date: Calendar.current.date(byAdding: .day, value: -6, to: Calendar.current.startOfDay(for: Date()))!, averageWeight: 95.1),
WeightChartData(date: Calendar.current.date(byAdding: .day, value: -5, to: Calendar.current.startOfDay(for: Date()))!, averageWeight: 94.2),
WeightChartData(date: Calendar.current.date(byAdding: .day, value: -4, to: Calendar.current.startOfDay(for: Date()))!, averageWeight: 94.4),
WeightChartData(date: Calendar.current.date(byAdding: .day, value: -3, to: Calendar.current.startOfDay(for: Date()))!, averageWeight: nil),
WeightChartData(date: Calendar.current.date(byAdding: .day, value: -2, to: Calendar.current.startOfDay(for: Date()))!, averageWeight: 93.5),
WeightChartData(date: Calendar.current.date(byAdding: .day, value: -1, to: Calendar.current.startOfDay(for: Date()))!, averageWeight: 93.6),
WeightChartData(date: Calendar.current.startOfDay(for: Date()), averageWeight: nil)
]
It is important to note that the array might include WeightChartData
-elements with .averageWeight == nill
, meaning no weight data exists on those days.
This is how I attempted to draw the chart (WeightViewModel.Data
is my "production" data array containing 7 WeightChartData
-elements). Note the conditional LineMark(...)
:
Chart(WeightViewModel.Data)
{ item in
if item.averageWeight != nil
{
LineMark(
x: .value("Date", item.date, unit: .weekday),
y: .value("Value", item.averageWeight!)
)
}
}
I use these two modifiers to ensure my y-Axis scale is around the min and max .averageWeight
of the data array.
.chartYScale(
domain: [ getBottomRange(), getTopRange() ]
)
.chartYAxis
{
AxisMarks(
position: .leading,
values: .automatic(desiredCount: getYMarkCount())
)
}
This (γ-axis-customization) works as expected. Still, here are getBottomRange()
, getTopRange()
and `getYMarkCount() if you think these might cause my x-axis-related issue:
func getTopRange() -> Int
{
return Int(round(WeightViewModel.maxWeightData()?.averageWeight ?? 0.0) + 1.0)
}
func getBottomRange() -> Int
{
return Int(round(WeightViewModel.minWeightData()?.averageWeight ?? 0.0) - 1.0)
}
func getYMarkCount() -> Int
{
return abs(getTopRange() - getBottomRange())
}
For the x-axis, I use this to create one AxisMark(...)
for every day (so 7 elements on the x-Axis, irrespective of the elements containing weight data or nil, like in Apple Health):
.chartXAxis
{
AxisMarks(values: .stride(by: .day))
{ date in
AxisGridLine()
AxisValueLabel(format: .dateTime.weekday(.narrow), centered: true)
}
}
Let me now show you what happen's if one day has .averageWeight == nil
:
- If all of WeightViewModel.Data elements have valid
.averageWeight
: the graph draws correctly (meaning drawing all 7 days on the x-Axis):
- If an element (that is not the first or last element) has
.averageWeight == nil
, the chart rightly skips this y-value while stil showing the day on the x-Axis:
like in Apple Health. In this example only the second to last element
has .averageWeight == nil
.
- The problem is when the first or last element’s
.averageWeight == nil
. Again, because theLineMark(..)
is only calledif item.averageWeight != nil
, this first or last data point is skipped. Only this time, because it’s the first or last element, my x-Axis scale gets disrupted. Instead of still showing 7 days, it will now remove the day that has no weightData from the chart, resulting in only having 6 days on the x-Axis:
In this example I set the last element's .averageWeight
to nil
.
Note that Thursday is no longer on the x-Axis.
This is not all too surprising, as when the first or last element get’s skippid, the chart doesn’t even know that it has to display 7 dates. But I still want the day (that has no .averageWeight) to be shown on the chart’s x-Axis (as my x-Axis should always have 7 elements representing the last 7 days).
How would one achieve the described chart behavior of always showing the full week even when having optional data that might be nil
?
Thank you for reading all of this and trying to help me.
答案1
得分: 0
就像我说的,我只是最近几周才开始使用Swift/SwiftUI,但显然你可以使用**.chartXScale(domain:)**来定义一个封闭的日期范围...
> domain:
图表中沿x轴的可能数据值。你可以使用封闭范围来定义数字或日期值的域(例如,0 ... 500)。. . . - 文档
我在最后一个元素后面添加一天后才奇怪地获得了我想要的行为:
.chartXScale(domain: WeightViewModel.Data.first!.date ... Calendar.current.date(byAdding: .day, value: 1, to: WeightViewModel.Data.last!.date)!
英文:
Like I said, I'm only using Swift/SwiftUI a couple of weeks now, but apparently you can use .chartXScale(domain:) with a closed date range...
> domain:
The possible data values along the x axis in the chart. You can define the domain with a ClosedRange for number or Date values (e.g., 0 ... 500) . . . - Documentation
I got my intended behavior oddly only after adding one day after the last element using:
.chartXScale(domain: WeightViewModel.Data.first!.date ... Calendar.current.date(byAdding: .day, value: 1, to: WeightViewModel.Data.last!.date)!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论