Charts: customising chartYAxis values

anyone figured out how to customise the Y axis values? id like to be able to process the. axis values and. then display them in the. format id like. eg. "1000" I'd like to display as "1k"

the furthest Ive been able to get is to get the axis to display a static text as the number ie "Y" in the code below. but I haven't been able to access the value. in the code below 'value' seems to be an AxisValue which stores the value but I can't seem to access it in a format I can process.

.chartYAxis {
                AxisMarks() { value in
                    AxisGridLine()
                    AxisTick()
                    AxisValueLabel {
                            Text("Y")
                    }
                }
            }

id like to be able to do something like this:

.chartYAxis {
                AxisMarks() { value in
                    AxisGridLine()
                    AxisTick()
                    AxisValueLabel {
                        if value > 1000000000000.0 {
                            Text("\(value / 1000000000000.0)t")
                        } else if value > 1000000000.0 {
                            Text("\(value / 1000000000.0)b")
                        } else if value > 1000000.0 {
                            Text("\(value / 1000000.0)m")
                        } else if value > 1000.0 {
                            Text("\(value / 1000.0)k")
                        }
                    }
                }
            }
Answered by ngb in 718374022

a .stride solution based on Olivers suggestion:

func myChart() -> some View {
        var yAxisMaxValue = 23532 //get the min and max values from your data
        var yAxisMinValue = -7633 //get the min and max values from your data

        let roundedYAxisMaxValue = roundUp(yAxisMaxValue, to: 2)
        let roundedYAxisMinValue = roundUp(yAxisMinValue, to: 2)
        let strideValue = max(abs(roundedYAxisMaxValue), abs(roundedYAxisMinValue)) / 3.0 //max 3 axis marks above and max 3 below zero

        return Chart {
            //your chart layout code
        }
        .chartYAxis {
            AxisMarks(values: .stride(by: strideValue)) {
                let value = $0.as(Double.self)!
                AxisGridLine()
                AxisTick()
                AxisValueLabel {
                    Text("\(self.abbreviateAxisValue(string: "\(value)"))")
                }
            }
        }
}

func abbreviateAxisValue(string: String) -> String {
        let decimal = Decimal(string: string)
        if decimal == nil {
            return string
        } else {
            if abs(decimal!) > 1000000000000.0 {
                return "\(decimal! / 1000000000000.0)t"
            } else if abs(decimal!) > 1000000000.0 {
                return "\(decimal! / 1000000000.0)b"
            } else if abs(decimal!) > 1000000.0 {
                return "\(decimal! / 1000000.0)m"
            } else if abs(decimal!) > 1000.0 {
                return "\(decimal! / 1000.0)k"
            } else {
                return "\(decimal!)"
            }
        }
}

//round up to x significant digits
func roundUp(_ num: Double, to places: Int) -> Double {
        let p = log10(abs(num))
        let f = pow(10, p.rounded(.up) - Double(places) + 1)
        let rnum = (num / f).rounded(.up) * f
        return rnum
    }

This is absolutely the wrong way to do it - It relies on debug descriptions - but until we find another solution, this will work:

.chartYAxis {
      AxisMarks() { value in
                    AxisGridLine()
                    AxisTick()
                    AxisValueLabel {
                        Text("\(abbreviateAxisValue(string: self.parseAxisValue(value: value) ?? ""))")
           }
     }
}

func parseAxisValue(value: AxisValue) -> String? {
        let input = String(describing: value)
        let regex = /\((\d*.0)|\((0)|\((-\d*.0)/

        if let match = input.firstMatch(of: regex) {
            return "\(match.1 ?? match.2 ?? match.3 ?? "")"
        }
        return nil
    }

func abbreviateAxisValue(string: String) -> String {
        let decimal = Decimal(string: string)
        if decimal == nil {
            return string
        } else {
            if abs(decimal!) > 1000000000000.0 {
                return "\(decimal! / 1000000000000.0)t"
            } else if abs(decimal!) > 1000000000.0 {
                return "\(decimal! / 1000000000.0)b"
            } else if abs(decimal!) > 1000000.0 {
                return "\(decimal! / 1000000.0)m"
            } else if abs(decimal!) > 1000.0 {
                return "\(decimal! / 1000.0)k"
            } else {
                return "\(decimal!)"
            }
        }
}

As a more general question, I'm trying to use the format: parameter of AxisValueLabel to format axis values of type Int. According to the documentation format: takes a FormatStyle so I would expect that something like this would work:

AxisValueLabel(format: .number.sign(strategy: .always()))

The code completion in Xcode offers this as an option, but any attempt to format a number like this throws a Ambiguous use of number error from the compiler. Am I missing something?

I found that instead of parsing the encapsulated AxisValue you can transform it into the orginal value.

For an Int I am doing it so:

.chartXAxis() { AxisMarks(values: .stride(by: 1)) { let month = $0.as(Int.self)! } }

The .stride(by: 1) overrides the automatic selection of marks.

just tried. using .stride works. have to calculate your own values but probably a better solution than parsing debug descriptions. would be nice if we could vary stride values for different ranges eg. below 0 and above 0 but I suppose that's a different problem.

Accepted Answer

a .stride solution based on Olivers suggestion:

func myChart() -> some View {
        var yAxisMaxValue = 23532 //get the min and max values from your data
        var yAxisMinValue = -7633 //get the min and max values from your data

        let roundedYAxisMaxValue = roundUp(yAxisMaxValue, to: 2)
        let roundedYAxisMinValue = roundUp(yAxisMinValue, to: 2)
        let strideValue = max(abs(roundedYAxisMaxValue), abs(roundedYAxisMinValue)) / 3.0 //max 3 axis marks above and max 3 below zero

        return Chart {
            //your chart layout code
        }
        .chartYAxis {
            AxisMarks(values: .stride(by: strideValue)) {
                let value = $0.as(Double.self)!
                AxisGridLine()
                AxisTick()
                AxisValueLabel {
                    Text("\(self.abbreviateAxisValue(string: "\(value)"))")
                }
            }
        }
}

func abbreviateAxisValue(string: String) -> String {
        let decimal = Decimal(string: string)
        if decimal == nil {
            return string
        } else {
            if abs(decimal!) > 1000000000000.0 {
                return "\(decimal! / 1000000000000.0)t"
            } else if abs(decimal!) > 1000000000.0 {
                return "\(decimal! / 1000000000.0)b"
            } else if abs(decimal!) > 1000000.0 {
                return "\(decimal! / 1000000.0)m"
            } else if abs(decimal!) > 1000.0 {
                return "\(decimal! / 1000.0)k"
            } else {
                return "\(decimal!)"
            }
        }
}

//round up to x significant digits
func roundUp(_ num: Double, to places: Int) -> Double {
        let p = log10(abs(num))
        let f = pow(10, p.rounded(.up) - Double(places) + 1)
        let rnum = (num / f).rounded(.up) * f
        return rnum
    }
Charts: customising chartYAxis values
 
 
Q