iosswiftswiftuichartsswiftui-charts

Getting the foregroundStyle of mark in SwiftUI


I want to build an app and I would like to know how to get the foregroundStyle that swift uses for the marks in Chart, so I can put a text beside the chart saying something like "You spent x amount of money and the category you spent most on was x category, with a foreground color matching the one on the chart (a donut chart), I have this code. Thanks and sorry if I made a spelling mistake English is not my first language.

I tried using the same as the foreground style but it doesn't work because the function is defined for chart values i think, I also tried looking it up online but I didn't find anything

Chart(data) { product in
  SectorMark(
    angle: .value(
      Text(verbatim: product.title),
      animate ? product.ammount: 0
    ),
    innerRadius: .ratio(0.618),
    angularInset: 2
  )
  .cornerRadius(5)
  .foregroundStyle(
    by: .value(
      Text(verbatim: product.title),
      product.title
    )
  )
}

Solution

  • ChartProxy provides a method that gives you the foreground color, given a plottable value - foregroundStyle(for:). Since this requires a ChartProxy, you would need to put your text in chartOverlay or chartBackground.

    Here is a complete example:

    struct Product: Hashable {
        let title: String
        let amount: Int
    }
    
    let products = [
        Product(title: "Product A", amount: 1),
        Product(title: "Product B", amount: 2),
        Product(title: "Product C", amount: 3),
        Product(title: "Product D", amount: 4),
    ]
    
    struct ContentView: View {
        var body: some View {
            Chart(products, id: \.title) { product in
                SectorMark(
                    angle: .value("Amount", product.amount),
                    innerRadius: .ratio(0.618),
                    angularInset: 2
                )
                .foregroundStyle(by: .value("Title", product.title))
            }
            .chartPlotStyle {
                $0
                    .frame(height: 200)
                    .padding(.bottom, 40)
            }
            .chartLegend(position: .top, alignment: .center)
            .chartOverlay(alignment: .bottom) { proxy in
                if let mostAbundantProduct = products.max(by: { $0.amount < $1.amount }),
                   let foregroundStyle = proxy.foregroundStyle(for: mostAbundantProduct.title) {
                    Text("The most abundant product is \(mostAbundantProduct.title)")
                        .foregroundStyle(foregroundStyle)
                }
            }
            .padding()
        }
    }
    

    I added the text "The most abundant product is ..." at the bottom of the chart. This might intersect the chart legend or the chart itself, so I moved the legend to the top of the chart using .chartLegend, and also added some bottom padding in chartPlotStyle.

    Output:

    enter image description here


    Note that the colors that are automatically generated for drawing the chart might not be suitable for text. It might have very little contrast with the background of the text, for example. If the number of sectors (i.e. the number of different products) is known at compile time, I would recommend picking your own colors for each product:

    let productColors: [String: Color] = [
        "Product A": Color.red,
        "Product B": Color.green,
        "Product C": Color.blue,
        "Product D": Color.purple,
    ]
    

    Then you can apply these colors to the chart using chartForegroundStyleScale.

    struct ContentView: View {
        var body: some View {
            VStack {
                Chart(products, id: \.title) { product in
                    SectorMark(
                        angle: .value("Amount", product.amount),
                        innerRadius: .ratio(0.618),
                        angularInset: 2
                    )
                    .foregroundStyle(by: .value("Title", product.title))
                }
                .chartForegroundStyleScale {
                    productColors[$0] ?? .clear
                }
                .chartPlotStyle {
                    $0.frame(height: 200)
                }
                .padding()
                
                if let mostAbundantProduct = products.max(by: { $0.amount < $1.amount }) {
                    Text("The most abundant product is \(mostAbundantProduct.title)")
                        .foregroundStyle(productColors[mostAbundantProduct.title] ?? .clear)
                }
            }
        }
    }