I'm curious, is there a way of excluding only certain Series from the automatically generated legends that Swift Charts provides?
In this example, how would I hide "Series 1", from the legend?
private struct PlotPoint {
let x: Double
let y: Double
}
private struct PlotSeries: Identifiable {
let id: Int
let name: String
let color: Color
let points: [PlotPoint]
}
private func chartPoints(height: Double) -> [PlotPoint] {
stride(from: 0.0, through: 1.0, by: 0.05).map { y in PlotPoint(x: y*y, y: y*height) }
}
private let data: [PlotSeries] = [
PlotSeries(id: 0, name: "Series 1", color: .blue, points: chartPoints(height: 0.25)),
PlotSeries(id: 1, name: "Series 2", color: .green, points: chartPoints(height: 0.5)),
PlotSeries(id: 2, name: "Series 3", color: .orange, points: chartPoints(height: 0.75)),
PlotSeries(id: 3, name: "Series 4", color: .purple, points: chartPoints(height: 1.0))
]
private func foregroundStyleForSeries(_ seriesName: String) -> Color {
return data.first { $0.name == seriesName }?.color ?? .gray
}
struct LegendMinusASeries: View {
var body: some View {
Chart {
ForEach(data) { series in
let plot = LinePlot(series.points, x: .value("x", \.x), y: .value("y", \.y))
plot // Splitting for compile speed (See https://stackoverflow.com/q/79476526/14840926)
.foregroundStyle(by: .value("Series", series.name))
}
}
.chartForegroundStyleScale(mapping: foregroundStyleForSeries)
.padding()
}
}
FWIW, I already know I can achieve this by using .foregroundStyle(_:)
instead of .foregroundStyle(by:)
for the only Series in question (and only if I define it before the other Series). But it feels like this is taking advantage of undocumented and somewhat odd behaviour in Swift Charts, and I'm wondering if there's a better way, more directly supported by the API.
It has been more than a week and it looks like there isn't a nice answer to this. So until someone suggests a better solution, I will share the two workarounds I have for anyone else who runs into this problem.
struct LegendMinusASeriesSolA: View {
var body: some View {
Chart {
ForEach(data) { series in
let plot = LinePlot(series.points, x: .value("x", \.x), y: .value("y", \.y))
if series.name == "Series 1" {
plot // Splitting for compile speed (See https://stackoverflow.com/q/79476526/14840926)
.foregroundStyle(series.color)
} else {
plot
.foregroundStyle(by: .value("Series", series.name))
}
}
}
.chartForegroundStyleScale(mapping: foregroundStyleForSeries)
.padding()
}
}
struct LegendMinusASeriesSolB: View {
var body: some View {
Chart {
ForEach(data) { series in
let plot = LinePlot(series.points, x: .value("x", \.x), y: .value("y", \.y))
if series.name == "Series 1" {
plot // Splitting for compile speed (See https://stackoverflow.com/q/79476526/14840926)
.foregroundStyle(.blue)
} else {
plot
.foregroundStyle(by: .value("Series", series.name))
}
}
}
.chartForegroundStyleScale(mapping: foregroundStyleForSeries)
.chartLegend {
HStack {
ForEach(data) { series in
if series.name != "Series 1" {
Image(systemName: "circle.fill")
.foregroundStyle(series.color)
Text(series.name)
.foregroundStyle(Color.secondary)
}
}
}
.font(.caption2)
}
.padding()
}
}
In LegendMinusASeriesSolA
, we are applying .foregroundStyle(_:)
instead of .foregroundStyle(by:)
to the data for the series in question. Note that due to what looks like a Swift Charts bug to me, this only works if the series in question is before the other series. Try changing the condition to if series.name == "Series 2" {
or any other Series, to see the bad behaviour. (You could mitigate this problem by making sure you always specify the omitted-from-legend series first - either use two loops, or sort the array of series used in the loop)
In LegendMinusASeriesSolB
, we are completely overriding the generation of the Legend, so we can easily omit the series in question.
Here's what the result looks like: