Search code examples
swiftswiftuiswiftui-charts

Reverse Y axis of SwiftUI Chart


Currently with the below default configuration from SwiftUI Charts, it is plotting top to bottom, i.e. minimum to maximum top to bottom. See the attached picture. I want the axis flipped so its plotting bottom to top.

struct HeartRateChart: View {
    let dateInterval: DateInterval
    let segments: [HeartRateSegment]
    let maxHeartRate: CGFloat
    let minHeartRate: CGFloat
    
    init(dateInterval: DateInterval, segments: [HeartRateSegment], maxHeartRate: Int, minHeartRate: Int) {
        self.dateInterval = dateInterval
        self.segments = segments
        self.maxHeartRate = CGFloat(maxHeartRate)
        self.minHeartRate = CGFloat(minHeartRate)
    }
    
    var body: some View {
        Chart {
            ForEach(segments, id: \.startDate) { segment in
                let yStart: CGFloat = CGFloat(segment.samples.min()?.heartRate ?? 0)
                let yEnd: CGFloat = CGFloat(segment.samples.max()?.heartRate ?? 0)
                RuleMark(x: .value("startDate", segment.startDate), yStart: yStart, yEnd: yEnd)
            }
        }
        .chartYScale(domain: [minHeartRate, maxHeartRate], range: minHeartRate...maxHeartRate)
        .aspectRatio(contentMode: .fit)
        .chartYAxis {
            AxisMarks(values: [minHeartRate, maxHeartRate])
        }
        .chartXScale(domain: [dateInterval.start, dateInterval.end])
        .chartPlotStyle { content in
            content.frame(height: 200)
        }
        .padding()
    }
}

heart rate chart


Solution

  • If I understand the problem correctly, the values and labels are not plotting with the minimum(on bottom) to maximum(on top). Try changing the values to plottable values. This should fix the values, labels and the domain range.

    Use PlottableValue's .value for yStart and yEnd in the RuleMark, like this:

    RuleMark(
        x: .value("startDate", segment.startDate),
        yStart: .value("min", yStart), 
        yEnd: .value("max", yEnd))
    

    The result:

    chart These are the heart rate ranges for the first 5 marks of the chart shown:

    plot data

    I'm pasting my complete test based on your example because I made some minor changes for syntax reasons and made some assumptions about how the HeartRateSegment was structured. Hopefully this offers some help if you haven't resolved it already.

        import SwiftUI
        import Charts
        
        struct ContentView: View {
            var body: some View {
                HeartRateChart(dateInterval: DateInterval(start: .now, end: Calendar.current.date(byAdding: .minute, value: 60, to: .now) ?? .now), segments: heartRateSegments, maxHeartRate: 132, minHeartRate: 77)
            }
        }
        
        struct HeartRateChart: View {
            let dateInterval: DateInterval
            let segments: [HeartRateSegment]
            let maxHeartRate: Int
            let minHeartRate: Int
            
            init(dateInterval: DateInterval, segments: [HeartRateSegment], maxHeartRate: Int, minHeartRate: Int) {
                self.dateInterval = dateInterval
                self.segments = segments
                self.maxHeartRate = maxHeartRate
                self.minHeartRate = minHeartRate
            }
            
            var body: some View {
                Chart {
                    ForEach(segments, id: \.startDate) { segment in
                        let yStart = segment.heartRate.min() ?? 0
                        let yEnd = segment.heartRate.max() ?? 0
                        RuleMark(x: .value("startDate", segment.startDate),
                                yStart: .value("min", yStart),
                                    yEnd: .value("max", yEnd))
                    }
                }
                .chartYScale(domain: [minHeartRate, maxHeartRate])
                .aspectRatio(contentMode: .fit)
                .chartYAxis {
                    AxisMarks(values: [minHeartRate, maxHeartRate])
                }
                .chartXScale(domain: [dateInterval.start, dateInterval.end])
                .chartPlotStyle { content in
                    content.frame(height: 100)
                }
                .padding()
            }
        }
        
        struct HeartRateSegment: Identifiable {
            let id: UUID = UUID()
            let startDate: Date
            let heartRate: [Int]
        }
        
        // Randomly creates some heartrate data. Each segment represents a minute.
        let heartRateSegments: [HeartRateSegment] = {
            var segments: [HeartRateSegment] = []
            
            for j in stride(from: 0, to: 3600, by: 60) { // every minute for 1 hour (in seconds)
                let minheartRate = Int.random(in: 77...107)
                let span = Int.random(in: 5...25)
                let heartRates = [minheartRate, minheartRate + span]
                print(heartRates)
                segments.append( HeartRateSegment(startDate: Date().addingTimeInterval(TimeInterval(j)), heartRate: heartRates))
            }
            
            return segments
        }()