SwiftUIでRader Chart

# SwiftUIでサクッとレーダーチャートを作りたいな
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263501/3abf62c6-bef1-b4ce-a22e-e9dca9d2d9d1.png)


``` RadarChartView.swift
//
//  RadarChartView.swift
//
import SwiftUI

struct RadarChart: View {
    var keys: [String]
    var values: [Double]
    var strokeColor: Color = Color.blue
    let maxValue: Double = 5.0 // 上限を5に設定

    var body: some View {
        GeometryReader { geometry in
            self.makeRadarChart(geometry: geometry)
        }
    }

    private func makeRadarChart(geometry: GeometryProxy) -> some View {
        let center = CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)
        let radius = min(geometry.size.width, geometry.size.height) / 2
        let angleAdjustment = -CGFloat.pi / 2 // 上向きに開始

        return ZStack {
            // レーダーチャートの各線とラベルを描画
            ForEach(Array(keys.enumerated()), id: \.offset) { index, label in
                self.lineAndLabel(at: index, in: keys.count, radius: radius, center: center, label: label, angleAdjustment: angleAdjustment)
            }

            // データポイントを結ぶ線と内部を塗りつぶし
            Path { path in
                for (index, value) in values.enumerated() {
                    let angle = 2 * .pi / CGFloat(values.count) * CGFloat(index) + angleAdjustment
                    let pointRadius = radius * (value / maxValue) // 上限を5として正規化
                    let point = CGPoint(x: center.x + cos(angle) * pointRadius, y: center.y + sin(angle) * pointRadius)

                    if index == 0 {
                        path.move(to: point)
                    } else {
                        path.addLine(to: point)
                    }
                }
                path.closeSubpath()
            }
            .fill(strokeColor.opacity(0.3))
            .overlay(
                Path { path in
                    for (index, value) in values.enumerated() {
                        let angle = 2 * .pi / CGFloat(values.count) * CGFloat(index) + angleAdjustment
                        let pointRadius = radius * (value / maxValue)
                        let point = CGPoint(x: center.x + cos(angle) * pointRadius, y: center.y + sin(angle) * pointRadius)

                        if index == 0 {
                            path.move(to: point)
                        } else {
                            path.addLine(to: point)
                        }
                    }
                    path.closeSubpath()
                }
                .stroke(strokeColor, lineWidth: 2)
            )
        }
    }

    private func lineAndLabel(at index: Int, in total: Int, radius: CGFloat, center: CGPoint, label: String, angleAdjustment: CGFloat) -> some View {
        let angle = 2 * .pi / CGFloat(total) * CGFloat(index) + angleAdjustment
        let endPoint = CGPoint(x: center.x + cos(angle) * radius, y: center.y + sin(angle) * radius)

        return ZStack {
            Path { path in
                path.move(to: center)
                path.addLine(to: endPoint)
            }
            .stroke(Color.gray, lineWidth: 1)

            Text(label)
                .position(x: center.x + cos(angle) * (radius + 20), y: center.y + sin(angle) * (radius + 20))
        }
    }
}


struct RadarChartView: View {
    var scoresKeys: [String] = [
        "性格", "見た目", "価値観", "金銭感", "コミュ力"
    ]
    var scoreValues: [Double] = [3, 3, 3, 3, 3]

    var body: some View {
        RadarChart(keys: scoresKeys, values: scoreValues)
            .frame(height: 300)
            .padding()
    }
}

#Preview {
    RadarChartView()
}



```

# メモリあり

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263501/8b26af7f-fd51-aa57-0fdd-4beba344bbd2.png)

``` RaderChart.swift
//
//  RadarChartView.swift
//  matchingAppManager
//
//  Created by 高橋直希 on 2023/11/29.
//
import SwiftUI

struct RadarChart: View {
    var keys: [String]
    var values: [Double]
    var strokeColor: Color = .blue
    let maxValue: Double = 5.0 // 上限を5に設定

    var body: some View {
        GeometryReader { geometry in
            self.makeRadarChart(geometry: geometry)
        }
    }

    private func makeRadarChart(geometry: GeometryProxy) -> some View {
        let center = CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)
        let radius = min(geometry.size.width, geometry.size.height) / 2
        let angleAdjustment = -CGFloat.pi / 2 // 上向きに開始
        let memoryLevels = 6 // メモリの数

        return ZStack {
            // メモリ（目盛り）を描画する円
            ForEach(0..<memoryLevels, id: \.self) { level in
                Circle()
                    .stroke(lineWidth: 1)
                    .opacity(0.5)
                    .frame(width: radius * 2 * CGFloat(level) / CGFloat(memoryLevels - 1), height: radius * 2 * CGFloat(level) / CGFloat(memoryLevels - 1))
                    .position(center)
            }

            // レーダーチャートの各線とラベルを描画
            ForEach(Array(keys.enumerated()), id: \.offset) { index, label in
                self.lineAndLabel(at: index, in: keys.count, radius: radius, center: center, label: label, angleAdjustment: angleAdjustment)
            }

            // 各軸に沿った目盛り
            ForEach(Array(keys.enumerated()), id: \.offset) { index, _ in
                ForEach(1..<memoryLevels, id: \.self) { level in
                    self.memoryLine(at: index, in: keys.count, radius: radius * CGFloat(level) / CGFloat(memoryLevels - 1), center: center, angleAdjustment: angleAdjustment)
                }
            }
            // データポイントを結ぶ線と内部を塗りつぶし
            Path { path in
                for (index, value) in values.enumerated() {
                    let angle = 2 * .pi / CGFloat(values.count) * CGFloat(index) + angleAdjustment
                    let pointRadius = radius * (value / maxValue) // 上限を5として正規化
                    let point = CGPoint(x: center.x + cos(angle) * pointRadius, y: center.y + sin(angle) * pointRadius)

                    if index == 0 {
                        path.move(to: point)
                    } else {
                        path.addLine(to: point)
                    }
                }
                path.closeSubpath()
            }
            .fill(strokeColor.opacity(0.3))
            .overlay(
                Path { path in
                    for (index, value) in values.enumerated() {
                        let angle = 2 * .pi / CGFloat(values.count) * CGFloat(index) + angleAdjustment
                        let pointRadius = radius * (value / maxValue)
                        let point = CGPoint(x: center.x + cos(angle) * pointRadius, y: center.y + sin(angle) * pointRadius)

                        if index == 0 {
                            path.move(to: point)
                        } else {
                            path.addLine(to: point)
                        }
                    }
                    path.closeSubpath()
                }
                .stroke(strokeColor, lineWidth: 2)
            )
        }
    }

    private func memoryLine(at index: Int, in total: Int, radius: CGFloat, center: CGPoint, angleAdjustment: CGFloat) -> some View {
        let angle = 2 * .pi / CGFloat(total) * CGFloat(index) + angleAdjustment
        let point = CGPoint(x: center.x + cos(angle) * radius, y: center.y + sin(angle) * radius)

        return Path { path in
            path.move(to: center)
            path.addLine(to: point)
        }
        .stroke(Color.gray, style: StrokeStyle(lineWidth: 1, dash: [4, 4]))
    }

    private func lineAndLabel(at index: Int, in total: Int, radius: CGFloat, center: CGPoint, label: String, angleAdjustment: CGFloat) -> some View {
        let angle = 2 * .pi / CGFloat(total) * CGFloat(index) + angleAdjustment
        let endPoint = CGPoint(x: center.x + cos(angle) * radius, y: center.y + sin(angle) * radius)

        var labelDistance = radius + 15

        if index == 1 || index == 4 {
            labelDistance += 20
        }
        let xPosition = center.x + cos(angle) * labelDistance
        let yPosition = center.y + sin(angle) * labelDistance

        return ZStack {
            Path { path in
                path.move(to: center)
                path.addLine(to: endPoint)
            }
            .stroke(Color.gray, lineWidth: 1)

            Text(label)
                .position(x: xPosition, y: yPosition)
        }
    }
}

struct RadarChartView: View {
    var scoresKeys: [String] = [
        "性格", "見た目", "価値観", "金銭感", "コミュ力"
    ]
    var scoreValues: [Double] = [3, 3, 3, 3, 3]

    var body: some View {
        RadarChart(keys: scoresKeys, values: scoreValues)
            .frame(height: 300)
            .padding()
    }
}

#Preview {
    RadarChartView()
}
```