In rule mark I do not want data to come between two points on the chart. My chart is like
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.
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
}