Search code examples
iosswiftuiswiftui-charts

Styling chart axes


I am updating an old app written in Objective C to SwiftUI. One thing I am struggling with is how to style the axes with SwiftUI Charts. The old app used CorePlot.

This is the CorePlot version:

enter image description here

And the SwiftUI version:

enter image description here

I cannot figure out how to code in SwiftUI to get the same result as in the old version:

  • No gridlines through the plot
  • A line for the x axis at the bottom
  • Small ticks
  • Color of the axis and ticks

This is my code so far:

Chart(data) {
    LineMark(
        x: .value("x", $0.x),
        y: .value("y", $0.y)
    )
    .interpolationMethod(.catmullRom)
    .lineStyle(StrokeStyle(lineWidth: 1))
}
.chartXAxis {
    AxisMarks(position: .bottom, values: .automatic) { _ in
        AxisValueLabel(anchor: .top)
    }
}
.chartXAxisLabel(position: .bottom, alignment: .center) {
    Text(chartProperties.xTitle)
}
.chartXScale(domain: [chartProperties.minX, chartProperties.maxX])

.chartYAxisLabel(position: .leading, alignment: .center) {
    Text(chartProperties.yTitle)
}
.chartYScale(domain: [chartProperties.minY, chartProperties.maxY])

.chartYAxis {
    AxisMarks(position: .leading)
}

chartProperties is a struct that has info such as axis titles and min and max values.


Solution

  • You can add the axis ticks simply using AxisTick, and the axis lines can be added with AxisGridLine. .foregroundStyle can be added to both of these.

    The grid lines "through" the chart that you see are also AxisGridLines added by SwiftUI by default. The defaults will not be added if you use the AxisMarks initialiser that takes a @AxisMarkBuilder closure.

    In total, you need 3 sets of AxisMarks for each axis:

    • one set for the axis labels, every 1 for the y axis, and every 100 for the x axis
    • one set for the axis ticks, every 0.2 for the y axis, and every 20 for the x axis
    • one set for the axis grid line. You only need one line for each axis, positioned at the minimum value of each axis' domain.

    Additionally, the tick marks should be offsetted, so that the grid lines go though the ticks.

    Here is a complete example, where the y axis is (-2, 2), and x axis is (0, 400).

    struct Foo: Identifiable {
        let id: Double
        let y: Double
    }
    
    
    let data: [Foo] = (0..<400).map { Foo(id: Double($0), y: log2(1 - Double.random(in: 0..<1)) / -10 - 1) }
    
    struct ContentView: View {
        var body: some View {
            Chart(data) {
                LineMark(
                    x: .value("x", $0.id),
                    y: .value("y", $0.y)
                )
                .interpolationMethod(.catmullRom)
                .lineStyle(StrokeStyle(lineWidth: 1))
            }
            .chartXAxisLabel(position: .bottom, alignment: .center) {
                Text("X Axis")
            }
            .chartXScale(domain: [0, 400])
            .chartXAxis {
                AxisMarks(position: .bottom, values: .stride(by: 100)) {
                    AxisValueLabel(anchor: .top)
                }
                AxisMarks(position: .bottom, values: .stride(by: 20)) { value in
                    if value.index != 0 { // the leftmost value does not need a tick
                        AxisTick(length: 8, stroke: .init(lineWidth: 1))
                            .offset(y: -4)
                    }
                }
                AxisMarks(position: .bottom, values: [0]) {
                    AxisGridLine(stroke: .init(lineWidth: 1))
                }
            }
            .chartYAxisLabel(position: .leading, alignment: .center) {
                Text("Y Label")
            }
            .chartYScale(domain: [-2, 2])
            .chartYAxis {
                AxisMarks(position: .leading, values: .stride(by: 1)) {
                    AxisValueLabel()
                }
                AxisMarks(position: .leading, values: .stride(by: 0.2)) { value in
                    if value.index != 0 { // the bottommost value does not need a tick
                        AxisTick(length: 8, stroke: .init(lineWidth: 1))
                            .offset(x: 4)
                    }
                }
                AxisMarks(position: .leading, values: [-2]) {
                    AxisGridLine(stroke: .init(lineWidth: 1))
                }
            }
            .padding()
            .aspectRatio(1, contentMode: .fit)
        }
    }
    

    enter image description here