How to make annotations like in Apple's Health App?

86 Views Asked by At

I'm trying to make a line chart with annotations similar to this Cardio Fitness chart in the Apple Health App. Is there a way to do it in Swift Charts?

// Data Model
struct TestWeight: Identifiable {
    var id = UUID()
    var weight: Double
    var date: Date

    init(id: UUID = UUID(), weight: Double, day: Int) {
        self.id = id
        self.weight = weight
        let calendar = Calendar.current
        self.date = calendar.date(from: DateComponents(year: 2023, month: 10, day: day))!
    }
}

// Test data
var weight: [TestWeight] = [
    TestWeight(weight: 69.4, day: 2),
    TestWeight(weight: 69.2, day: 3),
    TestWeight(weight: 70.0, day: 4),
    TestWeight(weight: 69.7, day: 5),
    TestWeight(weight: 69.0, day: 6),
    TestWeight(weight: 68.8, day: 7),
    TestWeight(weight: 68.0, day: 8)
]

Chart {
    ForEach(weight) { data in
        LineMark(
            x: .value("Day", data.date, unit: .day),
            y: .value("Weight", data.weight)
        )
        .foregroundStyle(Color.pink)
    }
}
.frame(height: 150)
.symbol {
    Circle()
        .fill(Color.pink)
        .frame(width: 8)
}
.chartXAxis {
    AxisMarks(values: .stride(by: .day)) { _ in
        AxisTick()
        AxisGridLine()
        AxisValueLabel(format: .dateTime.weekday(.abbreviated), centered: true)
    }
}
.chartYScale(domain: 62...70)

enter image description here

1

There are 1 best solutions below

0
Marcy On BEST ANSWER

To make those horizontal lines with annotations, RuleMark or RectangleMark can be used. Rectangle marks were used in this example:

enter image description here

RectangleMark has a height parameter which was used to create something similar to the thicker lines in Apple's Cardio chart. RectangleMark has multiple inits to create horizontal lines, vertical lines, squares, rectangles and matrices.

A cornerRadius set to 1/2 the height was added to give the horizontal lines round end caps:

RectangleMark(
    xStart: .value("Start", weight[0].date, unit: .day),
    xEnd: .value("End", weight[2].date, unit: .day),
    y: .value("Average", 69.5),
    height: 6
)
.foregroundStyle(.gray)
.cornerRadius(3)

Annotations were added to the rectangle marks:

.annotation(position: .top, alignment: .leading, spacing: 4) {
      Text("69.5 VO\u{2082} max")
       .font(.subheadline)
 }

Full code for better context:

struct ContentView: View {

    var body: some View {
        Chart() {
            ForEach(weight) { data in
                LineMark(
                    x: .value("Day", data.date, unit: .day),
                    y: .value("Weight", data.weight)
                )
                .foregroundStyle(.pink)
                .symbol {
                    Circle()
                        .fill(.pink)
                        .frame(width: 8)
                }
            }

            // Creates horizontal lines
            ForEach(lines) { line in
                RectangleMark(
                    xStart: .value("Start", weight[line.start].date, unit: .day),
                    xEnd: .value("End", weight[line.end].date, unit: .day),
                    y: .value("Average", line.average),
                    height: 6
                )
                .foregroundStyle(line.color)
                .cornerRadius(3)
                .annotation(position: .top, alignment: line.alignment, spacing: 4) {
                    Text("\(line.average)\(line.label)")
                        .font(.subheadline)
                }
            }
        }
        .frame(height: 250)
        .chartXAxis {
            AxisMarks(values: .stride(by: .day)) { _ in
                AxisTick()
                AxisGridLine()
                AxisValueLabel(format: .dateTime.weekday(.abbreviated), centered: true)
            }
        }
        .chartYScale(domain: 67...71)
    }
}


    // Data Model
struct TestWeight: Identifiable {
    var id = UUID()
    var weight: Double
    var date: Date
    
    init(id: UUID = UUID(), weight: Double, day: Int) {
        self.id = id
        self.weight = weight
        let calendar = Calendar.current
        self.date = calendar.date(from: DateComponents(year: 2023, month: 10, day: day))!
    }
}

    // Test data
var weight: [TestWeight] = [
    TestWeight(weight: 69.4, day: 2),
    TestWeight(weight: 69.2, day: 3),
    TestWeight(weight: 70.0, day: 4),
    TestWeight(weight: 69.7, day: 5),
    TestWeight(weight: 69.0, day: 6),
    TestWeight(weight: 68.8, day: 7),
    TestWeight(weight: 68.0, day: 8)
]

    //Data Model - data to create lines
    struct TestLine: Identifiable {
        var start: Int
        var end: Int
        var average: Double
        var label: String
        var color: Color
        var alignment: Alignment
        var id: Int
    }

  //Test Data - horizontal lines
    let lines: [TestLine] = [
        TestLine(start: 0, end: 2, average: 69.5, label: " VO\u{2082} max", color: .gray, alignment: .leading, id: 1),
        TestLine(start: 3, end: 6, average: 68.8, label: "", color: .red, alignment: .trailing, id: 2)
    ]