Search code examples
swiftswiftuiswiftui-animationswiftui-charts

SwiftUI Chart Text Animation Glitch with AxisMarks and AxisLabels


I've encountered an animation glitch affecting the text for AxisMarks and AxisLabels in the SwiftUI Chart framework. Below are the sample project code and video for reference:

Chart Animation Example

struct ContentView: View {
    @State var isDisplayingAge = true
    
    var body: some View {
        ChartView(data: ChartDataSample, isDisplayingAge: isDisplayingAge)
            .onTapGesture {
                withAnimation {
                    isDisplayingAge.toggle()
                }
            }
    }
}

struct ChartData: Identifiable {
    let age: Int
    let presence: Int
    let grade: Int
    let id = UUID()
}

struct ChartView: View {
    
    let data: [ChartData]
    let isDisplayingAge: Bool
    
    let xAxis = [0, 50, 100]
    
    var body: some View {
        
        VStack {
            Chart {
                ForEach(data) { chartData in
                    BarMark(x: .value("X Axis", isDisplayingAge ? chartData.age : chartData.presence),
                            y: .value("Y Axis", chartData.grade))
                }
            }
            .chartXAxis(content: {
                AxisMarks(position: .bottom, values: xAxis.map{$0}) { data in
                    AxisValueLabel(String(xAxis[data.index]) + (isDisplayingAge ? " years" : " days"))
                }
            })
            .chartXAxisLabel(content: {
                Text(isDisplayingAge ? "Data: age" : "Data: presence")
            })
        }
        .padding(.horizontal, 13)
    }
}

As you can observe, when I tap the chart to trigger a content change with animation, the text for AxisMarks (e.g., x years / x days) and AxisLabels (e.g., Data: age / Data: presence) animates unusually. A portion of the text is temporarily replaced by an ellipsis ("...") during the animation.

Interestingly, this issue occurs only in one direction of the animation. When transitioning from "year" to "day" or from "presence" to "age," the text simply gets replaced without any noticeable animation.

Can anyone explain what's happening here? Ideally, I'd like the texts to either fade into each other or be replaced without any animation.

Thank you.


Solution

  • This is because, when you are transitioning from a short label to a long label, it tries to fit the long label in the same space. Since it doesn't fit, it truncates it.

    To fix, you probably need to deliver the labels yourself and then make sure they don't get truncated. For example:

    AxisValueLabel() {
        Text(String(xAxis[data.index]) + (isDisplayingAge ? " years" : " days"))
            .frame(minWidth: 100, alignment: .leading)
    }
    

    Alternatively, if you don't want to set a fixed minimum width, you could use a ZStack and update visibility using .opacity:

    AxisValueLabel() {
        ZStack(alignment: .leading) {
            Text(String(xAxis[data.index]) + " years").opacity(isDisplayingAge ? 1 : 0)
            Text(String(xAxis[data.index]) + " days").opacity(isDisplayingAge ? 0 : 1)
        }
    }
    

    Simply setting .fixedSize() on the Text also works, but there is some unexpected movement during animation which you probably don't want to see.

    You can use the same technique(s) for the X-axis labels too.