swiftuiswiftui-charts

SwiftUI line chart colored by series


How do I specify a custom color for each series on a SwiftUI line chart? The number and desired colors of the series are determined at runtime. The closest fit I've found is chartForegroundStyleScale<DataValue, S>(_ mapping: KeyValuePairs<DataValue, S>), but it takes a KeyValuePairs, which appears to only support compile-time initialization.

struct DataPoint: Identifiable, Hashable {
    let series: String
    let x: Int
    let y: Int
    
    var id: DataPoint { self }
}

let data: [DataPoint] = [
    DataPoint(series: "A", x: 1, y: 1),
    DataPoint(series: "A", x: 2, y: 2),
    DataPoint(series: "B", x: 3, y: 1),
    DataPoint(series: "B", x: 4, y: 1),
]

// Hard-coded for this example. in practice, the series names and colors (and number of series) will vary at runtime.
let seriesColors: [String: Color] = [
    "A": Color.red,
    "B": Color.green
]

struct ContentView: View {
    var body: some View {
        Chart(data) {
            LineMark(
                x: .value("X", $0.x),
                y: .value("Y", $0.y),
                series: .value("Series", $0.series))
        }
        .chartForegroundStyleScale(seriesColors) // Error: Cannot convert value of type '[String : Color]' to expected argument type 'KeyValuePairs<DataValue, S>'
    }
}

Update: Making Color conform to Plottable and using foregroundStyle didn't work. The chart treats all points as if they are from the first series it encounters. In this example, it plots a single brown line instead of a brown line and a purple line. Here's the conformance code:

let colorsBySeries: [String: Color] = ["A": .brown, "B": .purple]

extension Color: Plottable {
    public init?(primitivePlottable: String) { self = colorsBySeries[primitivePlottable]! }
    public var primitivePlottable: String { colorsBySeries.first { $0.value == self }!.key }
}

This is how I applied the style to LineMark:

.foregroundStyle(colorsBySeries[$0.series]!)

Solution

  • You can use this method:

    nonisolated func chartForegroundStyleScale<Domain, Range>( domain: Domain, range: Range, type: ScaleType? = nil ) -> some View where Domain : ScaleDomain, Range : ScaleRange, Range.VisualValue : ShapeStyle

    Example:

    Chart(data) { 
                LineMark(
                    x: .value("X", $0.x),
                    y: .value("Y", $0.y)
                )
                .foregroundStyle(by: .value("Series", $0.series))
    }
    .chartForegroundStyleScale(
                    domain: Array(series.keys),
                    range: Array(series.values)
                )
    
    let series = ["A":.green, "B": .red]
    
    let data: [DataPoint] = [
            DataPoint(series: "A", x: 1, y: 1),
            DataPoint(series: "A", x: 2, y: 2),
            DataPoint(series: "B", x: 2, y: 2),
            DataPoint(series: "B", x: 1, y: 1)
        ]