案例:可拖拽的散点图与筛选器
概述
本节将实现一个交互式散点图,支持以下功能:
- 拖拽手势平移图表查看不同区域
- 双指缩放聚焦数据范围
- 动态筛选器控制数据展示范围
- 高亮选中的数据点并显示 Tooltip
实现步骤
1. 基础散点图结构
import Charts
struct DataPoint: Identifiable {
let id = UUID()
let x: Double
let y: Double
let category: String
}
struct ScatterPlotView: View {
@State private var data: [DataPoint] = generateRandomData()
var body: some View {
Chart(data) { point in
PointMark(
x: .value("X", point.x),
y: .value("Y", point.y)
)
.foregroundStyle(by: .value("Category", point.category))
}
}
}
2. 添加拖拽和缩放手势
@State private var scale: CGFloat = 1.0
@State private var offset: CGSize = .zero
var body: some View {
Chart(data) { ... }
.chartXScale(domain: visibleXRange)
.chartYScale(domain: visibleYRange)
.gesture(
DragGesture()
.onChanged { value in
offset.width += value.translation.width / scale
offset.height -= value.translation.height / scale
}
)
.gesture(
MagnificationGesture()
.onChanged { value in
scale = value.magnitude
}
)
}
3. 实现动态筛选器
@State private var selectedCategories: Set<String> = []
var filteredData: [DataPoint] {
guard !selectedCategories.isEmpty else { return data }
return data.filter { selectedCategories.contains($0.category) }
}
var body: some View {
VStack {
CategoryFilterView(selectedCategories: $selectedCategories)
Chart(filteredData) { ... }
}
}
struct CategoryFilterView: View {
@Binding var selectedCategories: Set<String>
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(["A", "B", "C"], id: \.self) { category in
Button(category) {
if selectedCategories.contains(category) {
selectedCategories.remove(category)
} else {
selectedCategories.insert(category)
}
}
.padding()
.background(selectedCategories.contains(category) ? Color.blue : Color.gray)
}
}
}
}
}
4. 添加交互高亮效果
@State private var selectedPoint: DataPoint?
var body: some View {
Chart(filteredData) { point in
PointMark(...)
.opacity(selectedPoint == point ? 1 : 0.7)
.annotation {
if selectedPoint == point {
Text("(\(point.x.formatted()), \(point.y.formatted()))")
.padding(4)
.background(Color.white.opacity(0.8))
}
}
}
.chartOverlay { proxy in
Color.clear
.contentShape(Rectangle())
.gesture(
SpatialTapGesture()
.onEnded { value in
let location = value.location
if let point = proxy.value(at: location, as: DataPoint.self) {
selectedPoint = point
}
}
)
}
}
完整实现代码
[GitHub Gist 链接] 包含完整实现和示例数据集
关键知识点
Chart视图与手势的配合使用- 使用
chartXScale和chartYScale实现动态范围控制 @State和@Binding管理交互状态chartOverlay实现点击检测- 组合手势处理(拖拽 + 缩放 + 点击)
扩展练习
- 添加重置视图按钮
- 实现矩形选择框多选数据点
- 添加数据导出功能
- 支持动态添加新数据点
