Tailwind CSSTailwind CSS
Home
  • Tailwind CSS 书籍目录
  • Vue 3 开发实战指南
  • React 和 Next.js 学习
  • TypeScript
  • React开发框架书籍大纲
  • Shadcn学习大纲
  • Swift 编程语言:从入门到进阶
  • SwiftUI 学习指南
  • 函数式编程大纲
  • Swift 异步编程语言
  • Swift 协议化编程
  • SwiftUI MVVM 开发模式
  • SwiftUI 图表开发书籍
  • SwiftData
  • ArkTS编程语言:从入门到精通
  • 仓颉编程语言:从入门到精通
  • 鸿蒙手机客户端开发实战
  • WPF书籍
  • C#开发书籍
learn
  • Java编程语言
  • Kotlin 编程入门与实战
  • /python/outline.html
  • AI Agent
  • MCP (Model Context Protocol) 应用指南
  • 深度学习
  • 深度学习
  • 强化学习: 理论与实践
  • 扩散模型书籍
  • Agentic AI for Everyone
langchain
Home
  • Tailwind CSS 书籍目录
  • Vue 3 开发实战指南
  • React 和 Next.js 学习
  • TypeScript
  • React开发框架书籍大纲
  • Shadcn学习大纲
  • Swift 编程语言:从入门到进阶
  • SwiftUI 学习指南
  • 函数式编程大纲
  • Swift 异步编程语言
  • Swift 协议化编程
  • SwiftUI MVVM 开发模式
  • SwiftUI 图表开发书籍
  • SwiftData
  • ArkTS编程语言:从入门到精通
  • 仓颉编程语言:从入门到精通
  • 鸿蒙手机客户端开发实战
  • WPF书籍
  • C#开发书籍
learn
  • Java编程语言
  • Kotlin 编程入门与实战
  • /python/outline.html
  • AI Agent
  • MCP (Model Context Protocol) 应用指南
  • 深度学习
  • 深度学习
  • 强化学习: 理论与实践
  • 扩散模型书籍
  • Agentic AI for Everyone
langchain
  • 案例:可拖拽的散点图与筛选器

案例:可拖拽的散点图与筛选器

概述

本节将实现一个交互式散点图,支持以下功能:

  1. 拖拽手势平移图表查看不同区域
  2. 双指缩放聚焦数据范围
  3. 动态筛选器控制数据展示范围
  4. 高亮选中的数据点并显示 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 链接] 包含完整实现和示例数据集

关键知识点

  1. Chart 视图与手势的配合使用
  2. 使用 chartXScale 和 chartYScale 实现动态范围控制
  3. @State 和 @Binding 管理交互状态
  4. chartOverlay 实现点击检测
  5. 组合手势处理(拖拽 + 缩放 + 点击)

扩展练习

  1. 添加重置视图按钮
  2. 实现矩形选择框多选数据点
  3. 添加数据导出功能
  4. 支持动态添加新数据点
Last Updated:: 5/18/25, 10:44 AM