SwiftUI - 在给定的框架(打包气泡图)中适合不同大小(彼此)的 X 圈
SwiftUI - Fit X Circles of varying size (wrt each other) in a given frame (Packed Bubble Chart)
有没有什么方法可以用 SwiftUI 创建类似的东西(w/o 使用 D3.js)-
// test data
@State private var data: [DataItem] = [
DataItem(title: "chrome", weight: 180, color: .green),
DataItem(title: "firefox", weight: 60, color: .red),
DataItem(title: "safari", weight: 90, color: .blue),
DataItem(title: "edge", weight: 30, color: .orange),
DataItem(title: "ie", weight: 50, color: .yellow),
DataItem(title: "opera", weight: 25, color: .purple)
]
这里在测试数据中,“weight”表示应该是哪一项larger/smaller。
我能想到的一种方法是在给定视图中使用相对于父级大小的 X 圆圈。但这本身会给定位和确保圆圈不相互接触或重叠带来问题。
这里不知道SpriteKit的用法?可以使用它还是可以仅使用 SwiftUI 组件来实现?
这很有趣,回到学校数学:)
这是我的结果。小圆圈都围绕第一个大圆圈对齐,我没有解决你图片中“opera”的定位问题,好像是不小心。
代码如下:
这是一个起点,没有安全检查
import SwiftUI
struct DataItem: Identifiable {
var id = UUID()
var title: String
var size: CGFloat
var color: Color
var offset = CGSize.zero
}
struct ContentView: View {
// test data
@State private var data: [DataItem] = [
DataItem(title: "chrome", size: 180, color: .green),
DataItem(title: "firefox", size: 60, color: .red),
DataItem(title: "safari", size: 90, color: .blue),
DataItem(title: "edge", size: 30, color: .orange),
DataItem(title: "ie", size: 50, color: .yellow),
DataItem(title: "opera", size: 25, color: .mint)
]
var body: some View {
ZStack {
ForEach(data, id: \.id) { item in
ZStack {
Circle()
.frame(width: CGFloat(item.size))
.foregroundColor(item.color)
Text(item.title)
}
.offset(item.offset)
}
}
// calculate and set the offsets - could be done at other time or place in code
.onAppear {
data[0].offset = CGSize.zero
data[1].offset = CGSize(width: (data[0].size + data[1].size) / 2, height: 0 )
var alpha = CGFloat.zero
for i in 2..<data.count {
// sides of the triangle from circle center points
let c = (data[0].size + data[i-1].size) / 2
let b = (data[0].size + data[i].size) / 2
let a = (data[i-1].size + data[i].size) / 2
alpha += calculateAlpha(a, b, c)
let x = cos(alpha) * b
let y = sin(alpha) * b
data[i].offset = CGSize(width: x, height: -y )
}
}
}
// Calculate alpha from sides - 1. Cosine theorem
func calculateAlpha(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat) -> CGFloat {
return acos(
( pow(a, 2) - pow(b, 2) - pow(c, 2) )
/
( -2 * b * c ) )
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
好的,你激励了我:) 加长版来了
- 独立结构
- 适应 parent !!
- 接受数据,间距,起始角度,方向(clockwise/counterclockwise)
框架只是为了适应 parent 尺寸:
你这样称呼它:
struct ContentView: View {
// graph data
@State private var data: [DataItem] = [
DataItem(title: "chrome", size: 180, color: .green),
DataItem(title: "firefox", size: 60, color: .red),
DataItem(title: "safari", size: 90, color: .blue),
DataItem(title: "edge", size: 30, color: .orange),
DataItem(title: "ie", size: 50, color: .yellow),
DataItem(title: "chrome", size: 120, color: .green),
DataItem(title: "firefox", size: 60, color: .red),
DataItem(title: "safari", size: 90, color: .blue),
DataItem(title: "edge", size: 30, color: .orange),
DataItem(title: "opera", size: 25, color: .mint)
]
var body: some View {
BubbleView(data: $data, spacing: 0, startAngle: 180, clockwise: true)
.font(.caption)
.frame(width: 300, height: 400)
.border(Color.red)
}
}
这是代码:
struct DataItem: Identifiable {
var id = UUID()
var title: String
var size: CGFloat
var color: Color
var offset = CGSize.zero
}
struct BubbleView: View {
@Binding var data: [DataItem]
// Spacing between bubbles
var spacing: CGFloat
// startAngle in degrees -360 to 360 from left horizontal
var startAngle: Int
// direction
var clockwise: Bool
struct ViewSize {
var xMin: CGFloat = 0
var xMax: CGFloat = 0
var yMin: CGFloat = 0
var yMax: CGFloat = 0
}
@State private var mySize = ViewSize()
var body: some View {
let xSize = (mySize.xMax - mySize.xMin) == 0 ? 1 : (mySize.xMax - mySize.xMin)
let ySize = (mySize.yMax - mySize.yMin) == 0 ? 1 : (mySize.yMax - mySize.yMin)
GeometryReader { geo in
let xScale = geo.size.width / xSize
let yScale = geo.size.height / ySize
let scale = min(xScale, yScale)
ZStack {
ForEach(data, id: \.id) { item in
ZStack {
Circle()
.frame(width: CGFloat(item.size) * scale,
height: CGFloat(item.size) * scale)
.foregroundColor(item.color)
Text(item.title)
}
.offset(x: item.offset.width * scale, y: item.offset.height * scale)
}
}
.offset(x: xOffset() * scale, y: yOffset() * scale)
}
.onAppear {
setOffets()
mySize = absoluteSize()
}
}
// taken out of main for compiler complexity issue
func xOffset() -> CGFloat {
let size = data[0].size
let xOffset = mySize.xMin + size / 2
return -xOffset
}
func yOffset() -> CGFloat {
let size = data[0].size
let yOffset = mySize.yMin + size / 2
return -yOffset
}
// calculate and set the offsets
func setOffets() {
if data.isEmpty { return }
// first circle
data[0].offset = CGSize.zero
if data.count < 2 { return }
// second circle
let b = (data[0].size + data[1].size) / 2 + spacing
// start Angle
var alpha: CGFloat = CGFloat(startAngle) / 180 * CGFloat.pi
data[1].offset = CGSize(width: cos(alpha) * b,
height: sin(alpha) * b)
// other circles
for i in 2..<data.count {
// sides of the triangle from circle center points
let c = (data[0].size + data[i-1].size) / 2 + spacing
let b = (data[0].size + data[i].size) / 2 + spacing
let a = (data[i-1].size + data[i].size) / 2 + spacing
alpha += calculateAlpha(a, b, c) * (clockwise ? 1 : -1)
let x = cos(alpha) * b
let y = sin(alpha) * b
data[i].offset = CGSize(width: x, height: y )
}
}
// Calculate alpha from sides - 1. Cosine theorem
func calculateAlpha(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat) -> CGFloat {
return acos(
( pow(a, 2) - pow(b, 2) - pow(c, 2) )
/
( -2 * b * c ) )
}
// calculate max dimensions of offset view
func absoluteSize() -> ViewSize {
let radius = data[0].size / 2
let initialSize = ViewSize(xMin: -radius, xMax: radius, yMin: -radius, yMax: radius)
let maxSize = data.reduce(initialSize, { partialResult, item in
let xMin = min(
partialResult.xMin,
item.offset.width - item.size / 2 - spacing
)
let xMax = max(
partialResult.xMax,
item.offset.width + item.size / 2 + spacing
)
let yMin = min(
partialResult.yMin,
item.offset.height - item.size / 2 - spacing
)
let yMax = max(
partialResult.yMax,
item.offset.height + item.size / 2 + spacing
)
return ViewSize(xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax)
})
return maxSize
}
}
有没有什么方法可以用 SwiftUI 创建类似的东西(w/o 使用 D3.js)-
// test data
@State private var data: [DataItem] = [
DataItem(title: "chrome", weight: 180, color: .green),
DataItem(title: "firefox", weight: 60, color: .red),
DataItem(title: "safari", weight: 90, color: .blue),
DataItem(title: "edge", weight: 30, color: .orange),
DataItem(title: "ie", weight: 50, color: .yellow),
DataItem(title: "opera", weight: 25, color: .purple)
]
这里在测试数据中,“weight”表示应该是哪一项larger/smaller。
我能想到的一种方法是在给定视图中使用相对于父级大小的 X 圆圈。但这本身会给定位和确保圆圈不相互接触或重叠带来问题。
这里不知道SpriteKit的用法?可以使用它还是可以仅使用 SwiftUI 组件来实现?
这很有趣,回到学校数学:) 这是我的结果。小圆圈都围绕第一个大圆圈对齐,我没有解决你图片中“opera”的定位问题,好像是不小心。
代码如下: 这是一个起点,没有安全检查
import SwiftUI
struct DataItem: Identifiable {
var id = UUID()
var title: String
var size: CGFloat
var color: Color
var offset = CGSize.zero
}
struct ContentView: View {
// test data
@State private var data: [DataItem] = [
DataItem(title: "chrome", size: 180, color: .green),
DataItem(title: "firefox", size: 60, color: .red),
DataItem(title: "safari", size: 90, color: .blue),
DataItem(title: "edge", size: 30, color: .orange),
DataItem(title: "ie", size: 50, color: .yellow),
DataItem(title: "opera", size: 25, color: .mint)
]
var body: some View {
ZStack {
ForEach(data, id: \.id) { item in
ZStack {
Circle()
.frame(width: CGFloat(item.size))
.foregroundColor(item.color)
Text(item.title)
}
.offset(item.offset)
}
}
// calculate and set the offsets - could be done at other time or place in code
.onAppear {
data[0].offset = CGSize.zero
data[1].offset = CGSize(width: (data[0].size + data[1].size) / 2, height: 0 )
var alpha = CGFloat.zero
for i in 2..<data.count {
// sides of the triangle from circle center points
let c = (data[0].size + data[i-1].size) / 2
let b = (data[0].size + data[i].size) / 2
let a = (data[i-1].size + data[i].size) / 2
alpha += calculateAlpha(a, b, c)
let x = cos(alpha) * b
let y = sin(alpha) * b
data[i].offset = CGSize(width: x, height: -y )
}
}
}
// Calculate alpha from sides - 1. Cosine theorem
func calculateAlpha(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat) -> CGFloat {
return acos(
( pow(a, 2) - pow(b, 2) - pow(c, 2) )
/
( -2 * b * c ) )
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
好的,你激励了我:) 加长版来了
- 独立结构
- 适应 parent !!
- 接受数据,间距,起始角度,方向(clockwise/counterclockwise)
框架只是为了适应 parent 尺寸:
你这样称呼它:
struct ContentView: View {
// graph data
@State private var data: [DataItem] = [
DataItem(title: "chrome", size: 180, color: .green),
DataItem(title: "firefox", size: 60, color: .red),
DataItem(title: "safari", size: 90, color: .blue),
DataItem(title: "edge", size: 30, color: .orange),
DataItem(title: "ie", size: 50, color: .yellow),
DataItem(title: "chrome", size: 120, color: .green),
DataItem(title: "firefox", size: 60, color: .red),
DataItem(title: "safari", size: 90, color: .blue),
DataItem(title: "edge", size: 30, color: .orange),
DataItem(title: "opera", size: 25, color: .mint)
]
var body: some View {
BubbleView(data: $data, spacing: 0, startAngle: 180, clockwise: true)
.font(.caption)
.frame(width: 300, height: 400)
.border(Color.red)
}
}
这是代码:
struct DataItem: Identifiable {
var id = UUID()
var title: String
var size: CGFloat
var color: Color
var offset = CGSize.zero
}
struct BubbleView: View {
@Binding var data: [DataItem]
// Spacing between bubbles
var spacing: CGFloat
// startAngle in degrees -360 to 360 from left horizontal
var startAngle: Int
// direction
var clockwise: Bool
struct ViewSize {
var xMin: CGFloat = 0
var xMax: CGFloat = 0
var yMin: CGFloat = 0
var yMax: CGFloat = 0
}
@State private var mySize = ViewSize()
var body: some View {
let xSize = (mySize.xMax - mySize.xMin) == 0 ? 1 : (mySize.xMax - mySize.xMin)
let ySize = (mySize.yMax - mySize.yMin) == 0 ? 1 : (mySize.yMax - mySize.yMin)
GeometryReader { geo in
let xScale = geo.size.width / xSize
let yScale = geo.size.height / ySize
let scale = min(xScale, yScale)
ZStack {
ForEach(data, id: \.id) { item in
ZStack {
Circle()
.frame(width: CGFloat(item.size) * scale,
height: CGFloat(item.size) * scale)
.foregroundColor(item.color)
Text(item.title)
}
.offset(x: item.offset.width * scale, y: item.offset.height * scale)
}
}
.offset(x: xOffset() * scale, y: yOffset() * scale)
}
.onAppear {
setOffets()
mySize = absoluteSize()
}
}
// taken out of main for compiler complexity issue
func xOffset() -> CGFloat {
let size = data[0].size
let xOffset = mySize.xMin + size / 2
return -xOffset
}
func yOffset() -> CGFloat {
let size = data[0].size
let yOffset = mySize.yMin + size / 2
return -yOffset
}
// calculate and set the offsets
func setOffets() {
if data.isEmpty { return }
// first circle
data[0].offset = CGSize.zero
if data.count < 2 { return }
// second circle
let b = (data[0].size + data[1].size) / 2 + spacing
// start Angle
var alpha: CGFloat = CGFloat(startAngle) / 180 * CGFloat.pi
data[1].offset = CGSize(width: cos(alpha) * b,
height: sin(alpha) * b)
// other circles
for i in 2..<data.count {
// sides of the triangle from circle center points
let c = (data[0].size + data[i-1].size) / 2 + spacing
let b = (data[0].size + data[i].size) / 2 + spacing
let a = (data[i-1].size + data[i].size) / 2 + spacing
alpha += calculateAlpha(a, b, c) * (clockwise ? 1 : -1)
let x = cos(alpha) * b
let y = sin(alpha) * b
data[i].offset = CGSize(width: x, height: y )
}
}
// Calculate alpha from sides - 1. Cosine theorem
func calculateAlpha(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat) -> CGFloat {
return acos(
( pow(a, 2) - pow(b, 2) - pow(c, 2) )
/
( -2 * b * c ) )
}
// calculate max dimensions of offset view
func absoluteSize() -> ViewSize {
let radius = data[0].size / 2
let initialSize = ViewSize(xMin: -radius, xMax: radius, yMin: -radius, yMax: radius)
let maxSize = data.reduce(initialSize, { partialResult, item in
let xMin = min(
partialResult.xMin,
item.offset.width - item.size / 2 - spacing
)
let xMax = max(
partialResult.xMax,
item.offset.width + item.size / 2 + spacing
)
let yMin = min(
partialResult.yMin,
item.offset.height - item.size / 2 - spacing
)
let yMax = max(
partialResult.yMax,
item.offset.height + item.size / 2 + spacing
)
return ViewSize(xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax)
})
return maxSize
}
}