0

How do I obtain colour imagery, or more generally, styling information for each legend entry to construct a custom legend in a chart in Swift Charts?

I have a chart here, with a legend positioned on the right, but using the content: argument of chartLegend does not pass any info into the closure to use. I would like to wrap the legend into a scroll view, so when there are too many entries, the chart will appear correctly on the screen, and the user can scroll through the legend below the chart.

Chart(points, id: \.self) { point in
     LineMark(
          x: .value("time/s", point.timestamp),
          y: .value("potential/mV", point.potential)
    )
    .foregroundStyle(by: .value("Electrode", point.electrode.symbol))
}
.chartLegend(position: .bottom)
// ...

Here is the chart with too many legend entries interfering with the chart sizing, resulting in cropping:

Cropped chart.

And here is the chart with only a few entries so that the chart is sized correctly, with no cropping, and the legend has text to discern between the electrodes they represent:

Same chart with fewer legend entries.

Any help is much appreciated.

Tahmid Azam
  • 566
  • 4
  • 16

3 Answers3

3

Not scrolling but the chart legend can be placed on the side with code like this:

.chartLegend(position: .trailing, alignment: .top)

This gives more room for a legend. The legend won't scroll but all the items will be listed even if going past the end of the chart. The frame height can be increased. The example shows a before(default) and after(using the code above).

enter image description here

The colors can be assigned manually using the chartForegroundStyleScale like this after the end of the chart:

    .chartForegroundStyleScale([
        "Hong Kong": Color.green,
        "London": Color.red,
        "Taipei": Color.purple,
        "New York": Color.teal,
        "Paris": Color.pink,
        "Sydney": Color.orange
    ])

f

Symbols can be created based on the series:

.symbol(by: .value("City", series.city))

enter image description here

Marcy
  • 4,611
  • 2
  • 34
  • 52
  • 2
    Thank you for your answer. Unfortunately, since I am going for a kind of 'scrolling' chart view where you can step through the whole plot, I need the edges of the chart to be flush with the edges of the frame to get a seamless look. I will tinker around with explicitly defining colours for different legend entries and constructing a custom legend in a scrollview beneath, and for now put it off to the side to at least make the view functional to the user. – Tahmid Azam Aug 06 '22 at 07:01
2

I'm not sure what you originally tried, but this works in Swift 5.7. Overriding the legend with .chartLegend() removes the legend colors, so I added func colorFor(symbol:) to cycle through the default legend colors. Note: "symbols" is an array of the electrode names, used in the legend.

import SwiftUI
import Charts

struct ContentView: View {
    let (symbols, points) = Point.sampleData()
    let colors = [Color.blue, .green, .orange, .purple, .red, .cyan, .yellow]

    var body: some View {
        Chart(points, id: \.self) { point in
            LineMark(
                x: .value("time/s", point.timestamp),
                y: .value("potential/mV", point.potential)
            )
            .foregroundStyle(by: .value("Electrode", point.electrode.symbol))
        }
        .chartLegend(position: .bottom) {
            ScrollView(.horizontal) {
                HStack {
                    ForEach(symbols, id: \.self) { symbol in
                        HStack {
                            BasicChartSymbolShape.circle
                                .foregroundColor(colorFor(symbol: symbol))
                                .frame(width: 8, height: 8)
                            Text(symbol)
                                .foregroundColor(.gray)
                                .font(.caption)
                        }
                   }
                }
                .padding()
            }
        }
        .chartXScale(domain: 0...0.5)
        .chartYAxis {
            AxisMarks(position: .leading)
        }
        .padding()
    }
    
    func colorFor(symbol: String) -> Color {
        let symbolIndex = symbols.firstIndex(of: symbol) ?? 0
        return colors[symbolIndex % colors.count]  // wrap-around colors
    }
}

SwiftUI Chart with scrolling legend

Here's the code I used to generate sample data, in case you were wondering.

import Foundation

struct Point: Hashable {
    var timestamp: Double
    var potential: Double
    var electrode: Electrode
    
    struct Electrode: Hashable {
        var symbol: String
    }
    
    static func sampleData() -> ([String], [Point]) {
        let numElectrodes = 20
        let numTimes = 100
        let maxTime = 0.5
        let amplitude = 80.0
        let frequency = 2.0  // Hz
        let bias = 30.0
        
        var symbols = [String]()
        var points = [Point]()
        
        for e in 0..<numElectrodes {
            let phase = Double.random(in: 0...10)
            let symbol = "C\(e + 1)"
            symbols.append(symbol)
            for t in 0..<numTimes {
                let time = maxTime * Double(t) / Double(numTimes)
                let potential = amplitude * sin(2 * Double.pi * frequency * time - phase) + bias
                points.append(Point(timestamp: time, potential: potential, electrode: .init(symbol: symbol)))
            }
        }
        return (symbols, points)
    }
    
    static func == (lhs: Point, rhs: Point) -> Bool {
        lhs.electrode == rhs.electrode && lhs.timestamp == rhs.timestamp
    }
}
P. Stern
  • 279
  • 1
  • 5
0

Apart from positioning (the other answer), you have complete freedom what your legend shows:

.chartLegend(position: .bottom) {
    HStack {
        Text("Value 1")
        Text("Value 2")
    }
}

This can help to overcome space restrictions, or highlight one in bold font, or to show something like the maximum value of a curve in the legend.

Gerd Castan
  • 6,275
  • 3
  • 44
  • 89