大多数浏览器和
Developer App 均支持流媒体播放。
-
探索 Swift Charts 中的饼图及交互性
Swift Charts 又回到了原点:准备好利用框架的最新改进以在 App 中制作饼图和环形图。了解如何让你的图表具备滚动功能,探索图表选择 API 以显示数据中的其他详细信息,并了解如何启用额外的交互功能使你的图表更加令人愉悦。
章节
- 0:20 - Pie charts
- 4:22 - Selection
- 7:49 - Scrolling
资源
相关视频
WWDC23
-
下载
♪ ♪
Richard:大叫好 我是 Richard 今天 我很高兴向你介绍 Swift Charts 中 一些令人兴奋的新功能: 饼图、选择和滚动 我们首先从饼图开始 Swift Charts 提供了可组合、 可定制的构建模块 可供你创建各种类型的数据可视化 最近 Swift Charts 新增了一个功能 有趣巧妙的饼图 饼图通过简单、常见的图形 展示总值的类别构成方式 例如 这张图表可视化了 我朋友餐车上的煎饼销售数据 饼图没有坐标轴 因此非常适用于对精度要求不高的 非正式场合 在广泛了解扇区是如何 构成完整圆形的基础上 扇区可以非常直观地 可视化部分与整体的关系 饼图深受用户喜爱的其中一个原因 就是其具备圆且直观的图形 你可以使用熟悉的 基于标记的组合语法 来创建饼图 在这里我们引入一个新的标记类型 SectorMark SectorMark 代表饼图中的 一个扇区 并位于极空间中 但可不是这个极地 而是极坐标系 并且 扇区大小 与其表示的数值成正比 沿着半径 你可以自定义扇区的外观 如果我增加内半径 饼图就会变成一个环形图 使用 SectorMark 你可以轻松构建各种饼图和环形图 我来给你举个例子 我们的朋友 共售卖 6 种口味的煎饼 其去年国际煎饼餐车业务的 日常销量大幅增长 这个夏天 我负责 帮助他们改进销售 App
首先 我用一个图表 可视化最畅销的煎饼口味 针对该目的 当前 App 中 有一个简单的堆积条形图 该图表使用了一个 BarMark 并在 X 轴方向堆积销量 由于这是分类数据 所以类别由各个堆积条的 前景样式进行表示 尽管这个图表能够满足要求 但我们还是将其转换为一个饼图 来充分利用屏幕上的可用空间 并将数据突出显示 而我所要做的就是 将 BarMark 和 x 参数 替换为 SectorMark 和角度就可以了 就是这么简单!
在 SectorMark 中 我使用角度来表示销量 接着 你为饼图提供的角度值 就会自动归一化为完整的圆形 并且 我还可以应用 一些样式自定义 在扇区上设置 angularInset 可以在扇区之间增加间隔 这里 我将扇区的 angularInset 设置为 1.5 点 从而两个扇区之间的间隔是 插入值的 2 倍 即宽为 3 点 此外 我还设置了 cornerRadius 来让饼图成为完美的圆形 只用了几行代码 这个饼图看上去就已经很不错了 但如果你还想尝试其他外观 我们来将该图表转换为环形图
我将 innerRadius 设置为 内半径和饼图半径的比值 对我来说 黄金比例看起来正合适 但你也可能对 环形图的厚度有不同的喜好 在所有已售的煎饼中 我们的赢家是 Cachapa 并已显示在图表上方 但由于环形图中间是空心的 我想将文本移动到图表中心 我将文本放入 chartBackground 中 并使用位置计算来保证文本 位于环形洞的中心
现在 这个环形图就很好看了 以上就是饼图和环形图 这两个图表可以很好地 突出显示你的数据 并可在大屏幕上呈现出色的效果 接下来 我来深入介绍一下 图表交互功能 首先 我们从选择开始 通过在表格中使用交互性 你可以逐步展示额外的细节信息 交互性支持用户 使用多种形式的输入 例如触摸 来自然地探索数据
选择便是你与图表 直接交互的一种方式 其中 Apple 设计的图表 例如心率图 便是很好的例子 只要选择坐标轴上的一点 图表就会显示额外的信息 让我们在煎饼销售 App 中 实现这个想法
该 App 中的一个图表可视化了 两个城市的每周日均销量
我将在该图表中启用数值选择 通过可以显示选择那天煎饼销量 的弹出框来展示具体销量
以下是该图表的定义方式 每个城市都有一个数据系列 并且系列中的每个元素都包含了 一星期中的某一天以及销量 然后 根据城市名字设置线条样式 你可能已经很熟悉 ChartOverlay 修饰符了 该修饰符可以让你覆盖 SwiftUI 视图来捕捉手势 但在 iOS 17 中 我可以使用新的 ChartXSelection 修饰符 该修饰符可以为我 处理所有的手势识别 并将选定的数值存储到绑定中
该选择修饰符会为我提供 X 轴上的原始日期值 所以 我可以定义一个计算属性 来与折线图中的数据点 进行匹配 接下来 我们来扩展图表 当数值被选中时会显示一个弹出框
在选中一个数值后 我会添加一个垂直标尺标记 作为选择指示器 我将其 Z 轴设置在 默认 0 值的下方 以保证标尺标记位于线条标记后方 现在 我们在选择指示器上方 创建一个弹出框 为此 我会将 自定义 SwiftUI 视图作为注释 通常情况下 注释位于图表中 但在本例中 弹出框 超出了图表的边界 所以 我需要处理注释溢出的情况
在 X 轴上 我将其设置为 适应图表大小 以确保弹出框 永远无法超出图表的水平边界 在 Y 轴上 我禁用了overflowResolution 从而可以在图表正上方显示注释
通过使用选择绑定 和带有注释的标尺标记 我便创建了一个交互式折线图 并且 Swift Charts 也支持 在 macOS 上进行选择 其中悬停手势是 数值选择的默认手势
除了单个数值选择 该图表选择修饰符的变体 还允许你选择一个范围 iOS 中的默认手势为双指轻点 而 macOS 中的则为拖动 此外 Swift Charts 还允许你 提供自定义的选择手势 使用 ChartProxy 你可以 基于手势位置选择一个数值
除了 X 和 Y 坐标上的图表之外 图表的数值选择还可与饼图和环形图 无缝结合 轻点并突出显示扇形 这样的操作非常有趣
所以归根结底 选择旨在展示 图表中额外的信息 构成交互性的另一重要部分 是浏览数据 接下来 我们来谈谈滚动
我想创建一个能够可视化 一整年每日煎饼销量的图表 但是将 365 天的数据 都显示在屏幕上不太现实 所以该图表必须支持滚动 为了启用滚动 我只需调用 chartScrollableAxes 修饰符 借助 chartXVisibleDomain 我设置一个 以 30 天为时间间隔的可见窗口 为了在当前可见窗口中 显示煎饼的总销量 我会使用 ChartScrollPosition 将当前日期存储到绑定中 现在 我只用手指进行滚动即可
不仅图表会滚动 轴的内容也会随之滚动 并且非常顺滑流畅 滚动可以通过几种不同的方式 进行自定义 例如 我想在滚动时 与日期单位保持对齐 这样我们就需要引入滚动操作 ScrollTargetBehavior 是 SwiftUI 和 Swift Charts 的 新增功能 可以让你将滚动视图内容 与数值对齐
为了实现我想要的对齐操作 我将其设置为 匹配每天的第一个小时 而 MajorAlignment 通过定义滑动操作 则可以实现进一步的自定义 这里 我将其设置为 每个月的第一天 这样 当我滑动浏览图表时 我就可以从每个月第一天开始
可滚动图表的构建 基于 SwiftUI 中 最新增强的滚动视图 想要了解更多信息 请观看“滚动视图外的内容”讲座 Swift Charts 为你提供了 可视化数据的无限可能 除了 X 和 Y 坐标上的图表 饼图现在也是一种 用于创建 Apple 设计图表的 API 饼图虽然简单 但可视化功能十分强大 并且在展示部分与整体数据关系时 可以发挥最大的优势 选择和滚动之类的交互功能 为你可视化数据提供了全新的维度 并让你的用户在探索数据中 拥有完全的掌控权 请尽情享受 制作饼图和环形图的乐趣吧 ♪ ♪
-
-
2:06 - Stacked bar chart
Chart(data, id: \.name) { element in BarMark( x: .value("Sales", element.sales), stacking: .normalized ) .foregroundStyle(by: .value("Name", element.name)) } .chartXAxis(.hidden)
-
2:44 - Pie chart
Chart(data, id: \.name) { element in SectorMark( angle: .value("Sales", element.sales) ) .foregroundStyle(by: .value("Name", element.name)) }
-
3:05 - Pie chart with angular inset
Chart(data, id: \.name) { element in SectorMark( angle: .value("Sales", element.sales), angularInset: 1.5 ) .foregroundStyle(by: .value("Name", element.name)) }
-
3:06 - Pie chart with corner radius
Chart(data, id: \.name) { element in SectorMark( angle: .value("Sales", element.sales), angularInset: 1.5 ) .cornerRadius(5) .foregroundStyle(by: .value("Name", element.name)) }
-
3:33 - Donut chart
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)) }
-
4:02 - Donut chart with text in the center
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()) .foregroundColor(.primary) } .position(x: frame.midX, y: frame.midY) } }
-
5:14 - Chart visualizing average sales by city
struct LocationDetailsChart: View { ... 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) } } ... } }
-
5:39 - Chart selection modifier
struct LocationDetailsChart: View { @Binding var rawSelectedDate: Date? 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) } } .chartXSelection(value: $rawSelectedDate) } }
-
5:47 - Processing raw selected date from chart selection binding
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) } } .chartXSelection(value: $rawSelectedDate) } }
-
6:06 - Rule mark as selection indicator
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) ) } } if let selectedDate { RuleMark( x: .value("Selected", selectedDate, unit: .day) ) .foregroundStyle(Color.gray.opacity(0.3)) .offset(yStart: -10) .zIndex(-1) } } .chartXSelection(value: $rawSelectedDate)
-
6:20 - Selection popover
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) ) } } 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 ) ) { valueSelectionPopover } } } .chartXSelection(value: $rawSelectedDate)
-
7:07 - Range selection
Chart(data) { series in ForEach(series.sales, id: \.day) { element in LineMark( x: .value("Day", element.day, unit: .day), y: .value("Sales", element.sales) ) } ... } .chartXSelection(value: $rawSelectedDate) .chartXSelection(range: $rawSelectedRange)
-
7:22 - Overriding default selection gesture
Chart(data) { series in ForEach(series.sales, id: \.day) { element in LineMark( x: .value("Day", element.day, unit: .day), y: .value("Sales", element.sales) ) } ... } .chartXSelection(value: $rawSelectedDate) .chartGesture { proxy in DragGesture(minimumDistance: 0) .onChanged { proxy.selectXValue(at: $0.location.x) } .onEnded { _ in selectedDate = nil } }
-
7:31 - Selection in pie charts and donut charts
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)
-
7:54 - Daily sales chart
Chart { ForEach(SalesData.last365Days, id: \.day) { BarMark( x: .value("Day", $0.day, unit: .day), y: .value("Sales", $0.sales) ) } .foregroundStyle(.blue) }
-
8:07 - Daily sales chart with a scrollable axis
Chart { ForEach(SalesData.last365Days, id: \.day) { BarMark( x: .value("Day", $0.day, unit: .day), y: .value("Sales", $0.sales) ) } .foregroundStyle(.blue) } .chartScrollableAxes(.horizontal)
-
8:11 - Setting the visible domain for a scrollable chart
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)
-
8:18 - Chart scroll position
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)
-
8:50 - Snapping in a scrolling chart
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))))
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。