I have a view that displays a chart based on an array. Each element in the array is all of the attributes associated with a core data entity. The data is properly displayed. I would like to animate the rendering of the charts data but cannot seem to figure out how to do it. If someone could point me in the right direction it would be appreciated. Below is the code.
struct ClosingValuesChart: View {
@State private var selectedDate: Date? = nil
@State private var selectedClose: Float? = nil
@State private var xAxisLabels: [Date] = []
var closingValues: [TradingDayClose] = []
var heading: String = ""
init(fundName: String, numYears: Int, closingValues: [TradingDayClose]) {
self.heading = fundName + String(" - \(numYears) Year")
self.closingValues = closingValues
}
var body: some View {
GroupBox (heading) {
let xMin = closingValues.first?.timeStamp
let xMax = closingValues.last?.timeStamp
let yMin = closingValues.map { $0.close }.min()!
let yMax = closingValues.map { $0.close }.max()!
let xAxisLabels: [Date] = GetXAxisLabels(xMin: xMin!, xMax: xMax!)
var yAxisLabels: [Float] {
stride(from: yMin, to: yMax + ((yMax - yMin)/7), by: (yMax - yMin) / 7).map { $0 }
}
Chart {
ForEach(closingValues) { value in
LineMark(
x: .value("Time", value.timeStamp!),
y: .value("Closing Value", value.close)
)
.foregroundStyle(Color.blue)
.lineStyle(StrokeStyle(lineWidth: 1.25))
}
}
.chartXScale(domain: xMin!...xMax!)
.chartXAxisLabel(position: .bottom, alignment: .center, spacing: 25) {
Text("Date")
.textFormatting(fontSize: 14)
}
.chartXAxis {
AxisMarks(position: .bottom, values: xAxisLabels) { value in
AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1))
AxisValueLabel(anchor: .top) {
if value.as(Date.self) != nil {
Text("")
}
}
}
}
.chartYScale(domain: yMin...yMax)
.chartYAxisLabel(position: .leading, alignment: .center, spacing: 25) {
Text("Closing Value")
.font(Font.custom("Arial", size: 14))
.foregroundColor(.black)
}
.chartYAxis {
AxisMarks(position: .leading, values: yAxisLabels) { value in
AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1))
AxisValueLabel() {
if let labelValue = value.as(Double.self) {
Text(String(format: "$ %.2f", labelValue))
.textFormatting(fontSize: 12)
}
}
}
}
.chartOverlay { proxy in
GeometryReader { geometry in
ChartOverlayRectangle(selectedDate: $selectedDate, selectedClose: $selectedClose, proxy: proxy, geometry: geometry, xMin: xMin!, xMax: xMax!, closingValues: closingValues)
}
}
.overlay {
XAxisDates(dateLabels: xAxisLabels)
}
.overlay {
DateAndClosingValue(selectedDate: selectedDate ?? Date(), selectedClose: selectedClose ?? 0.0)
}
}
.groupBoxStyle(ChartGroupBoxStyle())
}
}
I could not find a simple way to do this. But I've found a possible workaround. I've not tested with Charts, but here would be the principle that works for a simple ForEach.
- create an array of Bool with as many items as values, initiated to false
@State private var showData : [Bool]
In onAppear or when you hit a refresh button (where you first reset all showValue to false), execute this code:
var counter = 0
for value in closingValues {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2 * Double(counter)) { // adjust speed by changing 0.2
showData[counter] = true
}
counter += 1
}
}
Use it in ForEach
ForEach(Array(closingValues.enumerated()), id: \.offset) { (index, value) in
if showData[index] {
LineMark(
x: .value("Time", value.timeStamp!),
y: .value("Closing Value", value.close)
)
.foregroundStyle(Color.blue)
.lineStyle(StrokeStyle(lineWidth: 1.25))
}
}