Search code examples
arraysswiftuiswiftui-charts

Centering Text in SwiftUI Pie Chart and Adjusting Opacity for Selection


I'm working with SwiftUI to create a pie chart using SectorMark in a Chart view. I have two challenges:

  1. I'm struggling to center text in the middle of the pie chart. The text should display the selected token's balance, but it's not aligning correctly in the center.

  2. I want to set a low opacity for all sectors and then highlight the selected sector by changing its opacity to a brighter color. Any suggestions on how to center the text properly and adjust the sector opacity for selection would be greatly appreciated.

Here's the relevant part of my code:

struct TokenAllocationChartView: View {
    let tokenHolders: [Token]
    @Binding var selectedBalance: Double?
    
    var body: some View {
        ZStack {
            Chart(tokenHolders) { tokenHolder in
                SectorMark(
                    angle: .value("Balance", Int(tokenHolder.balance)!),
                    innerRadius: .ratio(0.618),
                    angularInset: 1.5
                )
                .cornerRadius(4)
                .foregroundStyle(by: .value("Names", tokenHolder.name))
            }
            .chartAngleSelection(value: $selectedBalance)
         
            .frame(height: 300)
            
            VStack(alignment:.center) {
                Text("Token Allocation")
                    .font(.callout)
                    .foregroundStyle(.secondary)
                
                Text(String(selectedBalance ?? 0.0))
                    .font(.system(size: 20))
                    .foregroundColor(.primary)
            }
        }
    }
}

Solution

  • I managed to solve your two issues thanks to a similar test app I had. Here's the code:

    let tokens: [Token] = [
        .init(name: "Mat", balance: "2000"),
        .init(name: "Jon", balance: "5000"),
        .init(name: "Fred", balance: "1200"),
        .init(name: "Tom", balance: "950"),
    ]
    
    struct ContentView: View {
        
        @State var selectedBalance: Double? = 0
        
        var body: some View {
            TokenAllocationChartView(
                tokenHolders: tokens,
                selectedBalance: $selectedBalance)
        }
        
    }
    
    struct TokenAllocationChartView: View {
        let tokenHolders: [Token]
        @Binding var selectedBalance: Double?
        @State private var barSelection: String?
        
        var body: some View {
            ZStack {
                Chart(tokenHolders) { tokenHolder in
                    SectorMark(
                        angle: .value("Balance", Int(tokenHolder.balance)!),
                        innerRadius: .ratio(0.618),
                        angularInset: 1.5
                    )
                    .cornerRadius(4)
                    .foregroundStyle(by: .value("Names", tokenHolder.name))
                    .opacity(barSelection == nil ? 1 : (barSelection! == tokenHolder.name ? 1 : 0.4))
                }
                .chartAngleSelection(value: $selectedBalance)
                .chartXSelection(value: $barSelection)
                
                .frame(height: 300)
                
                VStack(alignment:.center) {
                    Text("Token Allocation")
                        .font(.callout)
                        .foregroundStyle(.secondary)
                        .frame(height: 0) // <-- Add this line for center perfectly
                        
                    
                    Text(String(selectedBalance ?? 0.0))
                        .font(.system(size: 20))
                        .foregroundColor(.primary)
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
            }
            .onChange(of: selectedBalance, initial: false) { oldValue, newValue in
                if let newValue {
                    findToken(newValue)
                } else {
                    barSelection = nil
                }
            }
        }
        
        
        func findToken(_ rangeValue: Double) {
            var initalValue: Double = 0
            let convertedArray = tokens.compactMap { token -> (String, Range<Double>) in
                let rangeEnd = initalValue + (Double(token.balance) ?? .zero)
                let tuple = (token.name, initalValue..<rangeEnd)
                /// Updating inital value for next iteration
                initalValue = rangeEnd
                return tuple
            }
            
            /// Find the value in the range
            if let token = convertedArray.first(where: { $0.1.contains(rangeValue) }) {
                /// Update selection
                barSelection = token.0
            }
        }
    }
    
    struct Token: Identifiable {
        let id = UUID()
        var name: String
        var balance: String
    }
    

    The key part is the findToken function which takes in the current double value selected and finds the correct token name whose that fits that value. For higlighting the pie chart the opacity modifer is just what we need, and to center the Text inside the chart you just need the height to the Token Allocation text and set maxHeight for the enclosing VStack. The Result: Selecting Pie Chart