Search code examples
swiftui

In rule mark I do not want data to come between two points on the chart


In rule mark I do not want data to come between two points on the chart. My chart is like enter image description here When I click on a place on the graph that does not contain a point, that is, where I do not have data, the information of the rate appears as I marked it in black in the image. I don't want this to happen.

My model like this:

struct StockPriceData: Identifiable {
    var id = UUID()
    var timestamp: Date
    var open: Double
    var high: Double
    var low: Double
    var close: Double
}


var stockData: [StockPriceData] = [
    StockPriceData(timestamp: Date().addingTimeInterval(1696291200), open: 28.73093, high: 28.76422, low: 28.72581, close: 28.74253),
    StockPriceData(timestamp: Date().addingTimeInterval(1696294800), open: 28.7427, high: 28.75354, low: 28.70582, close: 28.75354),
    StockPriceData(timestamp: Date().addingTimeInterval(1696298400), open: 28.743, high: 28.79384, low: 28.73365, close: 28.75371),
    StockPriceData(timestamp: Date().addingTimeInterval(1696302000), open: 28.75486, high: 28.79107, low: 28.69415, close: 28.75723),
    StockPriceData(timestamp: Date().addingTimeInterval(1696305600), open: 28.75346, high: 28.77518, low: 28.69728, close: 28.75168),
    StockPriceData(timestamp: Date().addingTimeInterval(1696309200), open: 28.75148, high: 28.76248, low: 28.71292, close: 28.75618),
    StockPriceData(timestamp: Date().addingTimeInterval(1696312800), open: 28.7555, high: 28.77304, low: 28.69977, close: 28.76718),
    StockPriceData(timestamp: Date().addingTimeInterval(1696316400), open: 28.79195, high: 29.07266, low: 28.76586, close: 28.92544),
    StockPriceData(timestamp: Date().addingTimeInterval(1696320000), open: 28.92569, high: 29.07191, low: 28.75944, close: 28.82226),
    StockPriceData(timestamp: Date().addingTimeInterval(1696323600), open: 28.82452, high: 28.86211, low: 28.81454, close: 28.85591),
    StockPriceData(timestamp: Date().addingTimeInterval(1696327200), open: 28.85199, high: 28.88431, low: 28.84245, close: 28.86133),
    StockPriceData(timestamp: Date().addingTimeInterval(1696330800), open: 28.86129, high: 28.87093, low: 28.84494, close: 28.84873)
]

My UI is like this:

struct ContentView: View {
    
    @State var stockDatas: [StockPriceData]  = stockData
    @State var currentActiveItem: StockPriceData?
    @State private var selectedDate: Date?
    @State private var selectedClose: Double?
    
    var body: some View {
        ChartView
            .chartXScale(domain: stockDatas.map {$0.timestamp}.min()!...stockDatas.map{$0.timestamp}.max()!)
            .chartYScale(domain: stockDatas.map {$0.close}.min()!...stockDatas.map{$0.close}.max()!)
            .chartPlotStyle{
                chartPlotStyle($0)
            }
            .chartXAxis {
                AxisMarks(format: .dateTime.hour())
            }
    }
    
    private var ChartView: some View {
        Chart {
            ForEach(stockDatas) {
                LineMark(x: .value("time", $0.timestamp),
                         y: .value("price", $0.close))
                .foregroundStyle(Color.green)
                
                AreaMark(x: .value("time", $0.timestamp),
                         yStart: .value("Min", stockData.map{$0.close}.min()!),
                         yEnd: .value("Max", $0.close)
                ).foregroundStyle(LinearGradient(gradient: Gradient.init(colors: [Color.green, .clear]), startPoint: .top, endPoint: .bottom)).opacity(0.1)
                /*.accessibilityLabel("\($0.timestamp)")
                 .accessibilityValue("\($0.close) ")*/
            } .symbol(Circle())
            if let selectedDate = selectedDate, let selectedClose = selectedClose {
                
                RuleMark(x: .value("Selected Date", selectedDate))
                    .foregroundStyle(.gray)
                
                    .annotation(position: .top, alignment: .top) {
                        VStack {
                            Text("Close: \(selectedClose)")
                                .font(.system(size: 12))
                            Text("Date: \(selectedDate)")
                                .font(.system(size: 12))
                        }
                        .background(
                            RoundedRectangle(cornerRadius: 5)
                                .fill(Color.gray)
                        )
                        .foregroundColor(Color.white)
                    }
            }
            
        }.frame(width: UIScreen.main.bounds.width-23, height: 200)
            .chartOverlay { proxy in
                GeometryReader { geometry in
                    Rectangle().fill(.clear).contentShape(Rectangle())
                        .gesture(
                            DragGesture(minimumDistance: 0)
                                .onChanged { value in
                                    let origin = geometry[proxy.plotAreaFrame].origin
                                    let location = CGPoint(
                                        x: value.location.x - origin.x,
                                        y: value.location.y - origin.y
                                    )
                                    
                                    if let (date, close) = proxy.value(at: location, as: (Date, Double).self) {
                                        selectedDate = date
                                        selectedClose = close
                                    }
                                }
                                .onEnded { _ in
                                    selectedClose = nil
                                }
                        )
                }
            }
    }
    private func chartPlotStyle(_ plotContent: ChartPlotContent) -> some View {
        plotContent
            .frame(height: 200)
            .overlay{
                Rectangle()
                    .foregroundColor(.gray.opacity(0.5))
                    .mask( ZStack{
                        VStack{
                            Spacer()
                            Rectangle().frame(height: 1)
                        }
                        HStack{
                            Spacer()
                            Rectangle().frame(width: 0.3)
                            
                        }
                    }
                    )
            }
    }
}

I want only close and date values to appear on the points. Also, the date is wrong in Rulemark, how can I fix it? I have hourly data. I just want to show the entered data. I don't want the line chart to calculate extra close value.


Solution

  • Seems like you just need to find the data point in the data that is closest to the x coordinate of the touched location.

    func closestDatum(to date: Date) -> StockPriceData {
        return stockDatas.sorted { abs($0.timestamp.timeIntervalSince(date)) < abs($1.timestamp.timeIntervalSince(date)) }.first!
    }
    

    I use sorted here for simplicity. If this is too slow, try changing to a binary search instead, e.g. using the binarySearch extension from this answer, you can write:

    func closestDatum(to date: Date) -> StockPriceData {
        // a binary search could be faster,
        let index = stockDatas.binarySearch { $0.timestamp < date }
        if index == stockData.startIndex {
            return stockData[index]
        }
        let (a, b) = (stockData[index - 1], stockData[index])
        if date.timeIntervalSince(a.timestamp) < b.timestamp.timeIntervalSince(date) {
            return a
        } else {
            return b
        }
    }
    

    Then in the drag gesture, call closestDatum and set selectedDate and selectedClose to that datum.

    // value(atX:) is enough, we don't need the y value
    if let date = proxy.value(atX: location.x, as: Date.self) {
        let closest = closestDatum(to: date)
        selectedDate = closest.timestamp
        selectedClose = closest.close
    }