Photo by Luke Chesser on Unsplash.
苹果在2019年推出了SwiftUI,这是一种轻巧且易于使用的创建用户界面的方式。 本系列文章将从雷达图开始探讨SwiftUI框架如何帮助我们构建干净,简单和令人惊叹的数据可视化工具。柱形图表、环形图表、折线图表、综合图表的创建参见《SwiftUI从入门到实战》第七、八章的内容。
什么是雷达图?
雷达图(也称为网络图,蜘蛛图或Kiviat图)是一种表示在同一图中的多个变量的方法,这些变量在始于同一点但沿不同方向延伸的轴上。 下图显示了一个雷达图示例,而恰好正是我们在本文中构建的雷达图。 真是巧合!
我们在本文中构建的雷达图示例。
在以下情况下,这种类型的可视化可能是合适的:
- 您正在汇总一个大学中有多少学生被不同课程录取的概述。
- 您正在尝试可视化技能组的相对优势和劣势,以确定您需要练习哪些技能。
- 您正在展示进入公司各个部门的资金,以了解您如何花钱。
我们需要什么?
要创建这样的东西,我们需要阅读SwiftUI如何使用Shape和Path类型将图形渲染到显示器上。
路径的工作方式类似于UIKit的UIBezierPath和Core Graphics的CGPath。开发人员使用它来描述一个二维曲线,渲染系统可以在其绘制过程中绘制出该曲线。符合Shape协议的类型可以封装Path并确保渲染描述与绘制区域完全匹配。
形状和路径在本文中起着主导作用。现在我们对它们有了更多的了解,是时候开始编码了。
我们如何编码这种东西?
让我们从创建类似Web的标志性背景开始,这是Shape类型的完美用例。在执行此操作时,我们需要考虑两件事:
- 我们可能想重用此代码来为各种应用程序创建图表,并且我们的图表需要为每个数据集添加正确的轴数。
- 我们可能要更改网络的粒度。如果我们知道数据只能采用1到5之间的整数值,则添加一吨网格线是没有好处的。
struct RadarChartGrid: Shape {
let categories: Int
let divisions: Int
func path(in rect: CGRect) -> Path {
let radius = min(rect.maxX - rect.midX, rect.maxY - rect.midY)
let stride = radius / CGFloat(divisions)
var path = Path()
for category in 1 ... categories {
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.midX + cos(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * radius,
y: rect.midY + sin(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * radius))
}
for step in 1 ... divisions {
let rad = CGFloat(step) * stride
path.move(to: CGPoint(x: rect.midX + cos(-.pi / 2) * rad,
y: rect.midY + sin(-.pi / 2) * rad))
for category in 1 ... categories {
path.addLine(to: CGPoint(x: rect.midX + cos(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * rad,
y: rect.midY + sin(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * rad))
}
}
return path
}
}
我们的RadarChartGrid类型的代码可以做一些事情:
- 它实现了``形状''协议,从而可以调整``路径''的描述以适合其内部绘制的矩形。
- 它使开发人员可以控制数据中的类别数量以及在对轴进行分区时要进行的划分数量。
- 它根据可用的维度构造一个Path对象。
可以将轴视为车轮中的辐条。它们从中心的共同原点开始,并且长度都相同。这些属性使我们可以将它们视为假想圆的半径,并使用三角函数绘制它们。您可以通过查看path(in :)方法中的第一个for循环来研究细节。
我们也使用三角函数在两个轴之间绘制线。这些计算在嵌套(for :)方法的末尾的for循环中进行。
最后要注意的是我们将三角函数偏移了-π/ 2。偏移量使图表旋转,因此看起来更加平衡且视觉上令人愉悦。
绘制数据
我们使用绘制三角网的方式与绘制背景网的方式非常相似:使用三角法。看一下下面的代码:
struct RadarChartPath: Shape {
let data: [Double]
func path(in rect: CGRect) -> Path {
guard
3 <= data.count,
let minimum = data.min(),
0 <= minimum,
let maximum = data.max()
else { return Path() }
let radius = min(rect.maxX - rect.midX, rect.maxY - rect.midY)
var path = Path()
for (index, entry) in data.enumerated() {
switch index {
case 0:
path.move(to: CGPoint(x: rect.midX + CGFloat(entry / maximum) * cos(CGFloat(index) * 2 * .pi / CGFloat(data.count) - .pi / 2) * radius,
y: rect.midY + CGFloat(entry / maximum) * sin(CGFloat(index) * 2 * .pi / CGFloat(data.count) - .pi / 2) * radius))
default:
path.addLine(to: CGPoint(x: rect.midX + CGFloat(entry / maximum) * cos(CGFloat(index) * 2 * .pi / CGFloat(data.count) - .pi / 2) * radius,
y: rect.midY + CGFloat(entry / maximum) * sin(CGFloat(index) * 2 * .pi / CGFloat(data.count) - .pi / 2) * radius))
}
}
path.closeSubpath()
return path
}
}
我们的RadarChartPath在绘制任何内容之前会先检查一些先决条件。 如果检查失败,我们将返回一个空的Path对象,该对象将在屏幕上不可见。 如果它们通过了,我们将数据归一化到[0,1]范围内,并使用归一化的值以与以前相同的方式绘制路径。
放在一起
现在我们已经有了基础,现在是时候将所有内容放在一起以制作实际的图表视图了:
struct RadarChart: View {
var data: [Double]
let gridColor: Color
let dataColor: Color
init(data: [Double], gridColor: Color = .gray, dataColor: Color = .blue) {
self.data = data
self.gridColor = gridColor
self.dataColor = dataColor
}
var body: some View {
ZStack {
RadarChartGrid(categories: data.count, divisions: 10)
.stroke(gridColor, lineWidth: 0.5)
RadarChartPath(data: data)
.fill(dataColor.opacity(0.3))
RadarChartPath(data: data)
.stroke(dataColor, lineWidth: 2.0)
}
}
}
将所有内容组合在一起的代码非常简单。 我们将背景网页和数据形状放置在我们体内的ZStack中。 SwiftUI仍然很年轻,并且不提供轮廓和填充单个Shape类型的便捷方法,因此我们两次添加RadarChartPath(一个用于创建填充颜色,另一个用于创建轮廓)。
使用此相对较小和简单的代码,我们准备开始可视化我们的数据:
一个示例应用程序显示了我们的雷达图以及随机采样的数据集。
译自:https://betterprogramming.pub/data-visualization-with-swiftui-radar-charts-64124aa2ac0b