用 Swift Charts 实现数据分段

发表于

为您每周带来有关 Swift 和 SwiftUI 的精选资讯!

数据分段(Data Binning)是一种常用的数据处理技术,通常将连续的数值或时间数据划分为多个区间(这些区间在多数情况下是相邻且互不相交)。这种方法不仅涵盖了数据的完整范围,还为每个区间内的数据点提供了明确的界定。通过数据分段,我们可以更有效地分析、可视化和统计处理复杂的数据集。本文将探讨如何利用 Swift Charts 提供的先进 API 来实现精准而高效的数据分段。

Swift Charts 中的数据分段魔法

在我研究 Swift Charts 的过程中,我不仅关注其使用方法,更被其背后的设计理念和实现技巧所吸引。作为一个优秀的声明式图表框架,Swift Charts 提供了许多传统图表框架所不具备的强大功能。在大多数情况下,开发者只需进行简单的声明,框架就能自动完成复杂的图表绘制任务。

特别值得一提的是,当处理数字或日期类型的轴向数据时,Swift Charts 能够根据预设参数自动生成连续且不相交的区间。这一功能在其他图表框架中往往需要开发者自行实现,而 Swift Charts 则大大简化了这一过程。

让我们通过一个具体的例子来展示数据分段在 Swift Charts 中的应用:

Swift
let demoData: [ChartData] = [
  ChartData(date: Calendar.current.date(byAdding: .month, value: 0, to: Date())!, sales: 150, profit: 120),
  ChartData(date: Calendar.current.date(byAdding: .month, value: 1, to: Date())!, sales: 200, profit: 140),
  ChartData(date: Calendar.current.date(byAdding: .month, value: 3, to: Date())!, sales: 180, profit: 130),
  ChartData(date: Calendar.current.date(byAdding: .month, value: 4, to: Date())!, sales: 170, profit: 160),
]

struct ChartDemo: View {
  var body: some View {
    Chart {
      ForEach(["Sales", "Profit"], id: \.self) { series in
        ForEach(demoData) { entry in
          LineMark(
            x: .value("Date", entry.date), // X 轴:自动对日期数据进行分段处理
            y: .value(series, series == "Sales" ? entry.sales : entry.profit)
          )
          .symbol(by: .value("Type", series))
          .foregroundStyle(by: .value("Type", series))
        }
      }
    }
    .chartXAxis {
      // X 轴:按月份自动分段显示
      AxisMarks(values: .stride(by: .month)) { date in
        AxisValueLabel(format: .dateTime.month()) // 日期按月进行分段
        AxisGridLine()
      }
    }
    .chartYScale(domain: [50.0, 250]) // 设置 Y 轴的数据范围
    .chartYAxis {
      // Y 轴:在指定位置显示刻度线
      AxisMarks(values: [50, 150, 250]) { value in
        AxisGridLine()
        AxisValueLabel()
      }
    }
    .aspectRatio(1, contentMode: .fit)
    .padding(32)
  }
}

image-20240914101809474

在这个示例中,我们只需提供基础数据,Swift Charts 就能自动在 X 轴上生成相应的时间刻度,并按照指定的日期单位进行间隔。同样,在 Y 轴上,我们调整了数据显示范围,并在特定位置显示了刻度。这些功能的背后,其实都是数据分段技术在默默发挥作用。

虽然数据分段的基本概念看似简单, 但要实现一个高效、稳定的分段算法却面临诸多挑战。值得庆幸的是, Swift Charts 为我们提供了专门用于数据分段的 API:NumberBins 和 DateBins。这意味着即使我们不用它来构建图表, 也能轻松利用这些 API 来实现高效、稳定的数据分段, 并将其应用到其他多样化的场景中。这种设计不仅提高了框架的灵活性, 还为开发者处理各类数据分析任务提供了强大的工具支持。

NumberBins:用于数字的分段工具

NumberBins 是 Swift Charts 提供的一款强大的数字分段工具,适用于整数和浮点数。让我们通过一个示例来探索其基本用法:

Swift
let data = [100, 200, 250, 420, 500]
let bins = NumberBins(thresholds: data)

print(bins.thresholds) // 输出数据边界:[100, 200, 250, 420, 500]

// 遍历并显示分段后的数据区间
for value in bins {
    print(value)
}

print("count:", bins.count) // 输出区间个数:4
print("index:", bins.index(for: 300)) // 判断 300 属于哪个区间,返回索引 2(对应 250..<420)

// 输出结果
[100, 200, 250, 420, 500]
ChartBinRange<Int>(lowerBound: 100, upperBound: 200, isClosed: false)
ChartBinRange<Int>(lowerBound: 200, upperBound: 250, isClosed: false)
ChartBinRange<Int>(lowerBound: 250, upperBound: 420, isClosed: false)
ChartBinRange<Int>(lowerBound: 420, upperBound: 500, isClosed: true)
count: 4
index: 2

创建 NumberBins 实例后,我们可以获取以下信息:

  • thresholds:数据边界
  • 通过迭代获取分段后的数据区间
  • 使用 index(for:) 方法检查给定数据属于哪个区间

需要注意的是,index(for:) 方法始终会返回一个索引值。即使查询的数值不在任何实际的分段区间内,NumberBins 仍然会返回一个虚拟的索引。因此,在进行查询之前,建议先检查该数值是否在定义的分段范围内,以避免不期望的结果。

NumberBins 提供了多种构造方法,每种方法在创建区间时应用的规则略有不同:

image-20240914103741529

  1. NumberBins(thresholds:) 严格按照给定的阈值和顺序构建区间,不自动调整顺序,也不去重。
Swift
let data = [200, 100, 200, 250]
let bins = NumberBins(thresholds: data)
// 200..<100, 100..<200, 200...250
  1. NumberBins(data:desiredCount:minimumStride:) 根据 desiredCountminimumStride 综合决定如何分割数据,可能会扩展整体数据范围。
Swift
// 当 desiredCount 为 nil 时,采用 Scott's normal reference rule 自动计算分段数
let data = [100, 200, 800, 400, 500]
let bins = NumberBins(data: data) 
// thresholds: [0, 500, 1000] 

// 尽量按照 desiredCount 数量进行分段
let bins = NumberBins(data: [-100, 200, 800, 400, 500], desiredCount: 5)
// 结果:[-200, 0, 200, 400, 600, 800]

// 优先满足最小增量
let bins = NumberBins(data: [-100, 200, 800, 400, 500], desiredCount: 5, minimumStride: 400)
// 结果:[-500, 0, 500, 1000]
  1. NumberBins(range:desiredCount:minimumStride:)data 版本类似,但基于给定的范围:
Swift
let bins = NumberBins(range: 10...100, desiredCount: 3)
// 结果:[0, 25, 50, 75, 100]
// 注意:虽然 desiredCount 为 3,但 NumberBins 选择了更适合展示的分段数量,以确保每个分段长度一致

// 确保每个区域不小于 minimumStride
let bins = NumberBins(range: -50...100, minimumStride: 30)
// 结果:[-50, 0, 50, 100]
  1. 使用 size 设置固定的分段大小:
Swift
// 为保证分段大小一致,自动扩展了首尾两端
let bins = NumberBins(size: 30.0, range: -100.0...200.0)
// 结果:[-120.0, -90.0, -60.0, -30.0, 0.0, 30.0, 60.0, 90.0, 120.0, 150.0, 180.0, 210.0]

可以看出,NumberBins 不仅仅是简单地对数据进行分割,它需要综合考虑各种因素,特别是确保分段后的数据适合展示。这种智能的分段方式使得 NumberBins 成为处理数值数据的强大工具。

DateBins:用于日期的分段工具

DateBins 其用法和理念与 NumberBins 相似,作为日期分段工具,它的最大特点是能够根据指定的日历单位或时间间隔对日期进行智能分段。让我们通过示例来探索其功能:

Swift
// 设置中国时区的日历
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "Asia/Shanghai")!

// 定义日期范围:2024/5/5 - 2024/10/15
let startDate = calendar.date(from: DateComponents(year: 2024, month: 5, day: 5))!
let endDate = calendar.date(from: DateComponents(year: 2024, month: 10, day: 15))!

// 按照 “月” 进行分段
let bins = DateBins(unit: .month, range: startDate...endDate, calendar: calendar)

let dateFormatter = DateFormatter()
dateFormatter.calendar = calendar
dateFormatter.timeZone = calendar.timeZone
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"

// 输出阈值(分段点)
print("Thresholds")
for threshold in bins.thresholds {
  print(dateFormatter.string(from: threshold))
}

// 输出日期范围
print("Range:")
dateFormatter.dateFormat = "yyyy-MM-dd"
for value in bins {
  let rangeType = value.upperBound == bins.thresholds.last ? "..." : "..<"
  print(
    "\(dateFormatter.string(from: value.lowerBound))\(rangeType)\(dateFormatter.string(from: value.upperBound))")
}

// 输出:
Thresholds
2024-05-01 00:00:00 +0800
2024-06-01 00:00:00 +0800
2024-07-01 00:00:00 +0800
2024-08-01 00:00:00 +0800
2024-09-01 00:00:00 +0800
2024-10-01 00:00:00 +0800
2024-11-01 00:00:00 +0800
Range:
2024-05-01..<2024-06-01
2024-06-01..<2024-07-01
2024-07-01..<2024-08-01
2024-08-01..<2024-09-01
2024-09-01..<2024-10-01
2024-10-01...2024-11-01

除了使用日历单位,我们还可以根据具体的时间间隔进行分割:

Swift
let timeInterval:TimeInterval = 3 * 24 * 60 * 60 // 3 天
let bins = DateBins(timeInterval: timeInterval, range: startDate...endDate)

// 部分输出结果:
2024-05-05 00:00:00 +0800
2024-05-08 00:00:00 +0800
2024-05-11 00:00:00 +0800
...

DateBins 的这种灵活性使其成为处理时间序列数据的理想工具。无论是需要按月、周、日进行分段,还是需要自定义时间间隔,DateBins 都能轻松应对。这对于创建时间相关的图表、报告或数据分析任务特别有用。

值得注意的是,DateBins 会自动处理复杂的日历逻辑,如闰年、不同月份的天数,以及各种日历系统(如公历、农历、伊斯兰历等)的特殊规则。这种智能化的处理确保了分段结果在不同文化和地域背景下都能保持准确和一致。通过利用 Apple 的日历框架,DateBins 能够适应全球各地的日期表示方式,大大简化了开发者在处理跨文化、跨地域日期范围时的工作量。

总结

本文探讨了 Swift Charts 框架中两款强大的数据分段工具:NumberBinsDateBins。这些工具不仅为开发者提供了高效、稳定的数据分割能力,还具有多方面的优势:

  1. 精确性:它们能够智能处理各种复杂情况,如日期的特殊规则和数值的边界情况。
  2. 灵活性:无论是处理数值还是日期数据,这些工具都提供了多种分段方法,满足不同场景的需求。
  3. 系统集成:作为 Swift Charts 的一部分,这些 API 已经集成在苹果众多的系统框架中。使用它们不会增加应用的大小,是一种 “零成本” 的性能优化。
  4. 跨文化适应:特别是 DateBins,它能够适应全球各地的日期表示方式,为国际化应用提供了强大支持。

值得注意的是,苹果官方提供的众多框架中还蕴含着许多类似的宝藏工具,等待开发者去发掘和利用。这些工具不仅可以提高开发效率,还能确保应用与苹果生态系统的深度整合。

在未来的文章中,我们将继续探索和介绍更多这样的实用工具和 API。通过深入了解和巧妙运用这些系统级资源,我们可以构建出更高效、更流畅的应用。

每周一晚,与全球开发者同步,掌握 Swift & SwiftUI 最新动向
可随时退订,干净无 spam