arraysswiftswiftui-charts

How to Assign Unique Colors to Each BarMark in a SwiftUI Chart?


I am working on a SwiftUI app where I need to display a bar chart using the Charts framework. My chart displays a series of TokenHolder objects, and I want to assign a unique color to each BarMark in the chart.I want to color each bar differently based on the label generated in generateAlphabetLabels function. However, I'm not sure how to apply different colors to each BarMark in the Chart view. I tried using .chartForegroundStyleScale, but it doesn't seem to work as expected. How can I assign a unique color to each bar in the chart based on its label or any other unique property of the TokenHolder?

Here is my CryptoChartView struct:

import SwiftUI
import Charts

struct CryptoChartView: View {
    let cryptoData: [TokenHolder]

    func generateAlphabetLabels(count: Int) -> [String] {
        let letters = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
        var labels = [String]()
        
        for i in 0..<count {
            let prefixIndex = i / letters.count
            let suffixIndex = i % letters.count
            let prefix = prefixIndex > 0 ? String(letters[prefixIndex - 1]) : ""
            let suffix = String(letters[suffixIndex])
            labels.append(prefix + suffix)
        }
        
        return labels
    }

    var body: some View {
        let labels = generateAlphabetLabels(count: cryptoData.count)

        Chart {
            ForEach(Array(zip(labels, cryptoData)), id: \.1.id) { (label, holder) in
                BarMark(
                    x: .value("Wallet", "\(label)"),
                    y: .value("Amount", Double(holder.amount) ?? 0)
                )
            }
        }
        .chartScrollableAxes(.horizontal)
        .chartYAxis {
            AxisMarks(values: .automatic) { value in
                AxisGridLine(centered: true)
                AxisTick()
                AxisValueLabel() {
                    if let intValue = value.as(Int.self) {
                        Text(String.formatAmount(String(intValue)))
                    }
                }
            }
        }
    }
}

 

    static let sampleData = [
            TokenHolder(amount: "500000000000.000000000000000000", walletAddress: "0x1111111111111111111111111111111111111111", usdAmount: "75000000.00"),
            TokenHolder(amount: "450000000000.000000000000000000", walletAddress: "0x2222222222222222222222222222222222222222", usdAmount: "67500000.00"),
            TokenHolder(amount: "400000000000.000000000000000000", walletAddress: "0x3333333333333333333333333333333333333333", usdAmount: "60000000.00"),
            TokenHolder(amount: "350000000000.000000000000000000", walletAddress: "0x4444444444444444444444444444444444444444", usdAmount: "52500000.00"),
            TokenHolder(amount: "300000000000.000000000000000000", walletAddress: "0x5555555555555555555555555555555555555555", usdAmount: "45000000.00"),
            TokenHolder(amount: "250000000000.000000000000000000", walletAddress: "0x6666666666666666666666666666666666666666", usdAmount: "37500000.00"),
            TokenHolder(amount: "200000000000.000000000000000000", walletAddress: "0x7777777777777777777777777777777777777777", usdAmount: "30000000.00"),
            TokenHolder(amount: "150000000000.000000000000000000", walletAddress: "0x8888888888888888888888888888888888888888", usdAmount: "22500000.00"),

Solution

  • I don't know if it's the best solution, but it works.

    enter image description here

    You can add each color to your TokenHolder item e.g.

    struct TokenHolder: Identifiable, Codable {
        var id: String { walletAddress }
        let amount: String
        let walletAddress: String
        let usdAmount: String
        let color: Color
    }
    

    and add some essential staff to make it codable, identifiable and comparable to works well with charts e.g.

    extension TokenHolder: Comparable {
        static func < (lhs: TokenHolder, rhs: TokenHolder) -> Bool {
            lhs.amount < rhs.amount
        }
        
        static func == (lhs: TokenHolder, rhs: TokenHolder) -> Bool {
            lhs.amount == rhs.amount
        }
    }
    
    extension Color: Codable {
        enum CodingKeys: String, CodingKey {
            case red, green, blue
        }
        
        public init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            let r = try container.decode(Double.self, forKey: .red)
            let g = try container.decode(Double.self, forKey: .green)
            let b = try container.decode(Double.self, forKey: .blue)
            
            self.init(red: r, green: g, blue: b)
        }
    
        public func encode(to encoder: Encoder) throws {
            guard let colorComponents = self.colorComponents else {
                return
            }
            
            var container = encoder.container(keyedBy: CodingKeys.self)
            
            try container.encode(colorComponents.red, forKey: .red)
            try container.encode(colorComponents.green, forKey: .green)
            try container.encode(colorComponents.blue, forKey: .blue)
        }
    }
    
    fileprivate extension Color {
    
        var colorComponents: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)? {
            var r: CGFloat = 0
            var g: CGFloat = 0
            var b: CGFloat = 0
            var a: CGFloat = 0
            
            guard UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) else {
                return nil
            }
            
            return (r, g, b, a)
        }
    }
    

    And after that u can add to your BarMark modifier .foregroundStyle(holder.color).