SwiftUI Chart中如何复用月份列实现跨年份数据同列展示?
问题描述
我用以下Swift代码生成包含过去500天随机日期(chartDate)和随机销售额(sales)的数据集:
func dailySales2() -> [(chartDate: Date, year: String, sales: Int)] { var temp: [(chartDate: Date, year: String, sales: Int)] = [] for _ in 1...500 { let dateT = Calendar.current.date(byAdding: .day, value: -Int.random(in: 1...500), to: Date())! temp.append((chartDate: dateT, year: dateT.formatted(.dateTime.year()), sales: Int.random(in: 100...500))) } return temp.sorted(by: { $0.chartDate < $1.chartDate }) }
当前用SwiftUI Chart展示数据的代码如下:
Chart { ForEach(dailySales2(), id: \.chartDate) { chartD in BarMark( x: .value("Day", chartD.chartDate, unit: .month), y: .value("Sales", chartD.sales) ) .foregroundStyle(by: .value("Year:", chartD.year)) } } .chartXAxis { AxisMarks(values: .stride(by: .month)) { AxisValueLabel(format: .dateTime.month(.narrow), centered: true) } }
现在图表会生成2021年8月至2022年12月的月份列(A S O N D J F M A M J J A S O N D),我希望仅保留1-12月各一列,把不同年份的数据作为系列放在对应月份列中,该怎么实现?
解决方案
核心思路是将所有日期映射到同一个基准年份的对应月份,让不同年份的同月份数据对齐到X轴的同一位置,再通过年份区分系列。具体实现步骤如下:
1. 添加日期归一化工具方法
定义一个方法,将任意日期转换为基准年(比如2023年)的对应月份日期,确保同月份的日期都指向同一X轴位置:
func normalizedMonthDate(from date: Date) -> Date { let calendar = Calendar.current let month = calendar.component(.month, from: date) return calendar.date(from: DateComponents(year: 2023, month: month))! }
2. 修改Chart的X轴绑定逻辑
将BarMark的X值替换为归一化后的日期,同时保持unit: .month,这样X轴只会生成1-12月的列:
Chart { ForEach(dailySales2(), id: \.chartDate) { chartD in BarMark( x: .value("Month", normalizedMonthDate(from: chartD.chartDate), unit: .month), y: .value("Sales", chartD.sales) ) .foregroundStyle(by: .value("Year", chartD.year)) } } .chartXAxis { AxisMarks(values: .stride(by: .month)) { value in if let date = value.as(Date.self) { AxisValueLabel(format: .dateTime.month(.narrow), centered: true) } } }
3. 可选:聚合同月份销售额(避免数据重叠)
如果同一天存在多条数据,建议先聚合同月份同年份的总销售额,避免柱子重叠混乱:
func aggregatedSales() -> [(month: Int, year: String, totalSales: Int)] { let salesData = dailySales2() // 按(月份,年份)分组 let grouped = Dictionary(grouping: salesData) { item in (Calendar.current.component(.month, from: item.chartDate), item.year) } // 计算每组的总销售额并排序 return grouped.map { key, items in (month: key.0, year: key.1, totalSales: items.reduce(0) { $0 + $1.sales }) }.sorted { $0.month < $1.month } }
修改Chart的数据源为聚合后的数据:
Chart { ForEach(aggregatedSales(), id: \.month) { data in BarMark( x: .value("Month", Calendar.current.date(from: DateComponents(year: 2023, month: data.month))!, unit: .month), y: .value("Total Sales", data.totalSales) ) .foregroundStyle(by: .value("Year", data.year)) } } .chartXAxis { AxisMarks(values: .stride(by: .month)) { value in if let date = value.as(Date.self) { AxisValueLabel(format: .dateTime.month(.narrow), centered: true) } } }
内容的提问来源于stack exchange,提问作者Gizza Gander
相关产品推荐
相关产品推荐

