WWDC Quick Look 💓 By SwiftGGTeam
Explore pie charts and interactivity in Swift Charts

Explore pie charts and interactivity in Swift Charts

观看原视频

Highlight

Swift Charts 新增了 SectorMark 饼图类型、chartXSelection 选择 API 和 chartScrollableAxes 滚动能力,让数据可视化从静态展示进化为可交互探索。

核心内容

Richard 用一家煎饼餐车的销售数据贯穿整场演讲。这个场景选得巧妙——六种口味的销售占比天然适合饼图,365 天的日销售数据又需要滚动浏览,城市间的对比数据则展示了选择交互的价值。

01:13)饼图的核心是新引入的 SectorMark。它位于极坐标系中,扇形角度与数据值成正比。从柱状图改饼图只需要两行改动:把 BarMark 换成 SectorMark,把 x 参数换成 angle

02:35angularInset 控制扇形间距,cornerRadius 给扇形加圆角。把 innerRadius 设为半径比例(如 0.618)就得到甜甜圈图。甜甜圈中心的空洞可以用 chartBackground 放汇总信息。

04:24)选择交互是今年的重点。chartXSelection(value:) 修饰符处理所有手势识别,把选中的值绑定到一个状态变量。不需要自己写手势检测代码。选中后可以用 RuleMark 画一条垂直指示线,再用 annotation 显示详细信息弹窗。

07:07)除了单值选择,还有范围选择——chartXSelection(range:) 支持选取一段区间。默认手势按平台不同:iOS 使用双指轻点,macOS 使用拖拽。也可以完全自定义手势,通过 ChartProxy 把触摸位置转换成数据值。

07:31)饼图的选择用 chartAngleSelection(value:),选中扇形会自动高亮,其他扇形变暗。通过 opacity 修饰符可以控制未选中扇形的透明度。

07:54)可滚动图表解决了”数据点多到塞不下屏幕”的问题。chartScrollableAxes(.horizontal) 启用横向滚动,chartXVisibleDomain(length:) 设置可见窗口大小(以秒为单位的时间间隔)。chartScrollPosition(x:) 绑定当前滚动位置,可以显示当前窗口的汇总数据。

08:50)滚动对齐用 chartScrollTargetBehavior,支持按值对齐。演示代码让滚动停止时对齐到每天的第一个小时;用户大幅滑动时,majorAlignment 进一步要求落点匹配每月第一天。这样按月浏览销售数据时,每次都会停在月初。

详细内容

从柱状图到饼图

// 原来的柱状图
Chart(data, id: \.name) { element in
    BarMark(
        x: .value("Sales", element.sales),
        stacking: .normalized
    )
    .foregroundStyle(by: .value("Name", element.name))
}

// 改成饼图——只换 Mark 类型和参数名
Chart(data, id: \.name) { element in
    SectorMark(
        angle: .value("Sales", element.sales)
    )
    .foregroundStyle(by: .value("Name", element.name))
}

关键点:

  • 角度值自动归一化为完整圆,不需要手动计算百分比
  • foregroundStyle(by:) 自动为不同类别分配颜色

精致的甜甜圈图

Chart(data, id: \.name) { element in
    SectorMark(
        angle: .value("Sales", element.sales),
        innerRadius: .ratio(0.618),
        angularInset: 1.5
    )
    .cornerRadius(5)
    .foregroundStyle(by: .value("Name", element.name))
}
.chartBackground { chartProxy in
    GeometryReader { geometry in
        let frame = geometry[chartProxy.plotAreaFrame]
        VStack {
            Text("Most Sold Style")
                .font(.callout)
                .foregroundStyle(.secondary)
            Text(mostSold)
                .font(.title2.bold())
        }
        .position(x: frame.midX, y: frame.midY)
    }
}

关键点:

  • innerRadius: .ratio(0.618) 用黄金比例控制甜甜圈厚度
  • angularInset: 1.5 每个扇形内缩 1.5pt,实际间隙为 3pt
  • chartBackground 在图表绘制区域中心放置内容,用 GeometryReader 计算居中位置

带选择指示的折线图

struct LocationDetailsChart: View {
    @Binding var rawSelectedDate: Date?
    
    var selectedDate: Date? {
        guard let rawSelectedDate else { return nil }
        return data.first?.sales.first(where: {
            let endOfDay = endOfDay(for: $0.day)
            return ($0.day ... endOfDay).contains(rawSelectedDate)
        })?.day
    }
    
    var body: some View {
        Chart {
            ForEach(data) { series in
                ForEach(series.sales, id: \.day) { element in
                    LineMark(
                        x: .value("Day", element.day, unit: .day),
                        y: .value("Sales", element.sales)
                    )
                }
                .foregroundStyle(by: .value("City", series.city))
                .symbol(by: .value("City", series.city))
                .interpolationMethod(.catmullRom)
            }
            
            if let selectedDate {
                RuleMark(
                    x: .value("Selected", selectedDate, unit: .day)
                )
                .foregroundStyle(Color.gray.opacity(0.3))
                .offset(yStart: -10)
                .zIndex(-1)
                .annotation(
                    position: .top, spacing: 0,
                    overflowResolution: .init(
                        x: .fit(to: .chart),
                        y: .disabled
                    )
                ) {
                    ValuePopover(date: selectedDate)
                }
            }
        }
        .chartXSelection(value: $rawSelectedDate)
    }
}

关键点:

  • chartXSelection 返回的是原始坐标值,需要用计算属性匹配到最近的数据点
  • RuleMarkzIndex(-1) 确保指示线画在折线下方
  • overflowResolution 控制标注超出图表边界时的行为:X 轴限制在图表内,Y 轴允许超出
  • macOS 上选择默认用悬停手势,iOS 上用点击

可滚动的日销售图表

Chart {
    ForEach(SalesData.last365Days, id: \.day) {
        BarMark(
            x: .value("Day", $0.day, unit: .day),
            y: .value("Sales", $0.sales)
        )
    }
    .foregroundStyle(.blue)
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 3600 * 24 * 30)
.chartScrollPosition(x: $scrollPosition)
.chartScrollTargetBehavior(
    .valueAligned(
        matching: DateComponents(hour: 0),
        majorAlignment: .matching(DateComponents(day: 1))
    )
)

关键点:

  • chartXVisibleDomain(length:) 参数是秒数,30 天 = 3600 * 24 * 30
  • chartScrollPosition 绑定当前位置,可用于显示当前窗口的汇总统计
  • valueAligned 让滚动停止时对齐到指定时间单位,majorAlignment 控制翻页时的对齐粒度
  • 滚动基于 SwiftUI 的 ScrollView 增强,轴标签随内容同步滚动

饼图的选择交互

Chart(data, id: \.name) { element in
    SectorMark(
        angle: .value("Sales", element.sales),
        innerRadius: .ratio(0.618),
        angularInset: 1.5
    )
    .cornerRadius(5)
    .foregroundStyle(by: .value("Name", element.name))
    .opacity(element.name == selectedName ? 1.0 : 0.3)
}
.chartAngleSelection(value: $selectedAngle)

关键点:

  • chartAngleSelection 专门用于极坐标图表
  • 通过 opacity 修饰符实现选中高亮、未选中淡化
  • selectedAngle 需要与数据值对应,通常用计算属性转换

核心启发

  • 给健身 app 加一张周活动圆环的饼图变体:Apple Watch 的活动圆环本质上就是甜甜圈图。用 SectorMark 实现类似效果,中心显示总卡路里或总时长,点击扇形查看具体运动类型的明细。

  • 做一款可探索的财务支出应用:把月度支出按类别做成饼图,点击某个类别后下方展开该类别的时间趋势折线图。用 chartXSelection 在折线图上选择具体日期,显示当天的每笔交易明细。

  • 用可滚动图表展示长周期数据:股票、天气、健康类应用常有数百天的数据。用 chartScrollableAxes 配合 chartXVisibleDomain 只展示最近 30 天,用户左右滑动查看历史。scroll target behavior 让滑动总是对齐到月初或周一。

关联 Session

评论

GitHub Issues · utterances