How can I create this desired chart using Apple's SwiftUI Charts?
Desired Chart (mockup) - The stepped line graph with data points is in blue and the trend line is in red:
A trend line needs to be added to an existing chart that contains a stepped line graph. No SwiftUI Chart marks seemed to apply for a sloped trend line other than using another LineMark.
The trend line code, provided below, works when used with a scatter diagram(PointMark) only. But when the trend line is used with a line graph, that is, another LineMark then the trend line becomes a continuation of the first LineMark. When two LineMarks are used, the modifiers on the second LineMark are ignored.
Unwanted Result - When using two LineMarks in SwiftUI Charts, this is the unwanted result instead of the chart above:
Yet the trend line code works as desired when the other LineMark is removed:
The complete example code with example data:
import SwiftUI
import Charts
struct ContentView: View {
@State var showSteps: Bool = false
@State var showTrend: Bool = false
var body: some View {
VStack {
Chart {
//Main data
ForEach(dataPoints.filter({ data in data.type == .data}), id: \.id) { point in
if showSteps {
LineMark(
x: PlottableValue.value("Date", point.date),
y: PlottableValue.value("Amount", point.amount)
)
.interpolationMethod(.stepEnd)
}
PointMark(
x: .value("Date", point.date),
y: .value("Amount", point.amount)
)
}
// Trend line
if showTrend {
ForEach(dataPoints.filter({ data in data.type == .trend}), id: \.id) { trend in
LineMark(
x: PlottableValue.value("Date", trend.date),
y: PlottableValue.value("Amount", trend.amount)
)
.foregroundStyle(Color.red)
.lineStyle(StrokeStyle(lineWidth: 5))
.interpolationMethod(.linear)
}
}
}
.padding(.all)
.frame(height: 500)
.chartXScale(domain: Date(timeIntervalSinceNow: 3600 * 24 * -15) ... Date(timeIntervalSinceNow: 3600 * 24 * 15))
.chartYAxis {
AxisMarks(preset: .aligned, position: .automatic, values: .stride(by: 5)) {
let value = $0.as(Int.self) ?? 0
AxisGridLine()
AxisValueLabel(String("\(value)"))
}
}
//.chartScrollableAxes(.horizontal) // future use
Group {
Toggle("Show Steps", isOn: $showSteps)
Toggle("Show Trend", isOn: $showTrend)
}.padding(.horizontal)
}
}
}
enum DataType {
case data
case trend
}
struct DataPoint: Hashable, Identifiable {
var id : UUID = UUID()
var type : DataType
var date : Date
var amount : Double
}
let dataPoints: [DataPoint] = [
DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -14), amount: 100),
DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -13), amount: 97),
DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -10), amount: 85),
DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -9), amount: 84),
DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -8), amount: 82),
DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -7), amount: 78),
DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -6), amount: 75),
DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -5), amount: 68),
DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -4), amount: 65),
DataPoint(id: UUID(), type: .data, date: Date(timeIntervalSinceNow: 3600 * 24 * -3), amount: 50),
DataPoint(id: UUID(), type: .data, date: Date(), amount: 48),
DataPoint(id: UUID(), type: .trend, date: Date(timeIntervalSinceNow: 3600 * 24 * -15), amount: 100),
DataPoint(id: UUID(), type: .trend, date: Date(timeIntervalSinceNow: 3600 * 24 * 15), amount: 0)
]
A solution will need to work with a scrollable chart as the final chart will use a large timeframe. (scrollable modifier included but commented out)
Tried different interpolation methods on linear graphs, splitting the data into two arrays, using a chart overlay and creating a trendline with a CGPath. One of these might be a solution but I may have missed how to properly set it up.
Any tips on how to get the desired trend line working correctly with another LineMark? Or how to create a trend line without using a LineMark?
The line marks for the trend line and for the actual data should be in different series. Otherwise they are assumed to be in the same series and the points will be joined together.
Add a series:
parameter and give them different PlottableValue
s.
if showSteps {
LineMark(
x: .value("Date", point.date),
y: .value("Amount", point.amount),
series: .value("Series", "Actual Data")
)
.interpolationMethod(.stepEnd)
}
if showTrend {
ForEach(dataPoints.filter({ data in data.type == .trend}), id: \.id) { trend in
LineMark(
x: .value("Date", trend.date),
y: .value("Amount", trend.amount),
series: .value("Series", "Trend")
)
.foregroundStyle(Color.red)
.lineStyle(StrokeStyle(lineWidth: 5))
.interpolationMethod(.linear)
}
}