Swift Charts中AreaMark在特定数据结构下失效的原因及解决方法
我尝试用Swift Charts的AreaMark可视化数据,但不同数据结构会影响结果。以下两个结构体存储了相同的数据,但组织方式不同:
import SwiftUI import Charts struct Food: Identifiable { let name: String let sales: Int let day: Int let id = UUID() init(name: String, sales: Int, day: Int) { self.name = name self.sales = sales self.day = day } } struct Sales: Identifiable { let day: Int let burger: Int let salad: Int let steak: Int let id = UUID() var total: Int { burger + salad + steak } init(day: Int, burger: Int, salad: Int, steak: Int) { self.day = day self.burger = burger self.salad = salad self.steak = steak } }
使用第一个Food结构体时,所有图表类型都能正常工作;但第二个Sales结构体用BarMark和PointMark没问题,用AreaMark就会失效——注释掉三个AreaMark中的两个,剩下的一个能正常显示,多个共存时就出问题。
复现代码如下:
struct ExperimentView: View { let cheeseburgerSalesByItem: [Food] = [ .init(name: "Burger", sales: 29, day: 1), .init(name: "Salad", sales: 35, day: 1), .init(name: "Steak", sales: 30, day: 1), .init(name: "Burger", sales: 32, day: 2), .init(name: "Salad", sales: 38, day: 2), .init(name: "Steak", sales: 42, day: 2), .init(name: "Burger", sales: 35, day: 3), .init(name: "Salad", sales: 29, day: 3), .init(name: "Steak", sales: 41, day: 3), .init(name: "Burger", sales: 29, day: 4), .init(name: "Salad", sales: 38, day: 4), .init(name: "Steak", sales: 39, day: 4), .init(name: "Burger", sales: 43, day: 5), .init(name: "Salad", sales: 42, day: 5), .init(name: "Steak", sales: 30, day: 5), .init(name: "Burger", sales: 45, day: 6), .init(name: "Salad", sales: 39, day: 6), .init(name: "Steak", sales: 31, day: 6), .init(name: "Burger", sales: 37, day: 7), .init(name: "Salad", sales: 35, day: 7), .init(name: "Steak", sales: 30, day: 7), ] let cheeseburgerSalesByDay: [Sales] = [ .init(day: 1, burger: 29, salad: 35, steak: 30), .init(day: 2, burger: 32, salad: 38, steak: 42), .init(day: 3, burger: 35, salad: 29, steak: 41), .init(day: 4, burger: 29, salad: 38, steak: 39), .init(day: 5, burger: 43, salad: 42, steak: 30), .init(day: 6, burger: 45, salad: 39, steak: 31), .init(day: 7, burger: 37, salad: 35, steak: 30) ] var body: some View { VStack { Chart(cheeseburgerSalesByItem) { sale in AreaMark( x: .value("Day", sale.day), y: .value("Sales", sale.sales) ) .foregroundStyle(by: .value("Food Item", sale.name)) } .chartXScale(domain: 1...7) .padding() Spacer() Chart(cheeseburgerSalesByDay) { sale in AreaMark( x: .value("Day", sale.day), y: .value("Burger", sale.burger) ) .foregroundStyle(.brown) AreaMark( x: .value("Day", sale.day), y: .value("Salad", sale.salad) ) .foregroundStyle(.green) AreaMark( x: .value("Day", sale.day), y: .value("Steak", sale.steak) ) .foregroundStyle(.red) } .padding() } } }
请问问题出在哪里?如果想使用第二种Sales数据结构绘制AreaMark图表,应该怎么做?
问题原因
Swift Charts绘制AreaMark时,需要为每个数据系列(比如Burger、Salad、Steak)构建连续的点序列。在你当前的Sales结构体写法中,每个Sales实例对应一天的全品类数据,你在同一个Chart的循环里添加三个AreaMark,相当于给每个Day生成了三个独立的、仅包含单个点的系列——这种情况下图表无法将分散的单点连接成完整的区域。
而Food结构体的写法是把每个品类的每日数据拆成独立实例,图表能自动识别同一个品类的所有点属于同一系列,从而正确连接成区域。
解决方案
有两种可行的解决方式:
方式1:将Sales数据转换为扁平结构(复用Food逻辑)
在使用前把cheeseburgerSalesByDay转换成[Food]格式,这样就能直接复用第一个图表的写法:
var flattenedSales: [Food] { cheeseburgerSalesByDay.flatMap { daySales in [ Food(name: "Burger", sales: daySales.burger, day: daySales.day), Food(name: "Salad", sales: daySales.salad, day: daySales.day), Food(name: "Steak", sales: daySales.steak, day: daySales.day) ] } }
之后在Chart中使用flattenedSales即可。
方式2:为每个品类单独构建数据序列
不转换原始数据,而是为每个品类单独提取每日数据,作为独立序列传入Chart:
Chart { // Burger系列 ForEach(cheeseburgerSalesByDay) { sale in AreaMark( x: .value("Day", sale.day), y: .value("Sales", sale.burger) ) } .foregroundStyle(.brown) .seriesName("Burger") // Salad系列 ForEach(cheeseburgerSalesByDay) { sale in AreaMark( x: .value("Day", sale.day), y: .value("Sales", sale.salad) ) } .foregroundStyle(.green) .seriesName("Salad") // Steak系列 ForEach(cheeseburgerSalesByDay) { sale in AreaMark( x: .value("Day", sale.day), y: .value("Sales", sale.steak) ) } .foregroundStyle(.red) .seriesName("Steak") } .chartXScale(domain: 1...7)
这种写法的核心是:每个品类的所有AreaMark都被包裹在同一个ForEach中,图表会识别这些点属于同一系列,从而正确绘制出连续的区域。
内容的提问来源于stack exchange,提问作者Calvin

