swiftswiftuiswiftui-charts

How can we exclude a given series from Swift Chart Legend?


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()
    }
}

Chart with all Series in Legend

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.


Solution

  • 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:

    Chart with one of the series omitted from the legend