Nicolai Henriksen
Nicolai Henriksen

Reputation: 1504

Multiple Y axis in single Swift Charts?

I have two data sets and I want to show them in the same chart.

Both data sets are time based (x axis) and with same time range. They have different units of measure and scaling (y axis).

Is it possible to show both data sets in the same chart, but with two different y axis definitions?

I have tried to overlay two independent charts but then the two charts are not aligned as I configure one with Y axis to the left and the other with Y axis to the right.

Any ideas how to make this happen?

Upvotes: 1

Views: 3615

Answers (2)

ChrisR
ChrisR

Reputation: 12165

Here is an approach overlaying two separate Charts in a ZStack. By this you don't have to renormalise any values.

To make the charts align they both use the same double Y axes (with respective dummy values). Also the foreground color of the 2nd X axis is set to .clear to avoid overlaying type.

            ZStack {
                Chart {
                    ForEach(dataKosten, id: \.0) { item in
                        LineMark(
                            x: .value("Monat", item.0, unit: .month),
                            y: .value("Kosten", item.1)
                        )
                    }
                    .lineStyle(StrokeStyle.init(lineWidth: 3))
                    .foregroundStyle(.primary)
                    
                }
                .chartXAxis {
                    AxisMarks(values: .stride(by: .month)) { _ in
                        AxisTick()
                        AxisValueLabel(format: .dateTime.month(.wide), centered: true)
                    }
                }
                .chartYScale(domain: 0...250_000)
                .chartYAxis {
                    AxisMarks(values: .stride(by: 25_000)) { _ in
                        AxisGridLine()
                        AxisTick()
                        AxisValueLabel(format: .currency(code: "eur"), centered: false)
                            .foregroundStyle(Color.accentColor)
                    }
                    // dummy axis to align both charts
                    AxisMarks(position: .leading, values: [50]) { _ in
                        AxisGridLine()
                        AxisTick()
                        AxisValueLabel(centered: false)
                            .foregroundStyle(.clear)
                    }
               }


                Chart {
                    ForEach(dataFTE, id: \.0) { item in
                        BarMark(
                            x: .value("Monat", item.0, unit: .month),
                            y: .value("FTE", item.1)
                        )
                    }
                    .foregroundStyle(Color.secondary)
                }

                .chartXAxis {
                    AxisMarks(values: .stride(by: .month)) { _ in
                        AxisTick()
                        AxisValueLabel(format: .dateTime.month(.wide), centered: true)
                            .foregroundStyle(.clear)
                    }
                }
                .chartYScale(domain: 0...50) // important, so the value of dummy axis is not used for scaling
                .chartYAxis {
                    AxisMarks(position: .leading, values: Array(stride(from: 0, through: 50, by: 5))) { _ in
                        AxisGridLine()
                        AxisTick()
                        AxisValueLabel(centered: false)
                    }
                    // dummy axis to align both charts
                    AxisMarks(values: [100_000]) { _ in
                        AxisGridLine()
                        AxisTick()
                        AxisValueLabel(format: .currency(code: "eur"), centered: false)
                            .foregroundStyle(.clear)
                    }
               }
            }

Upvotes: 1

jknlsn
jknlsn

Reputation: 423

Here is an example showing two different data sets on the same axis. You need to scale the data yourself though, so in this example I've done so manually with the adjustments to the pressure values, but you could also do this programatically.

Example here at github.com/jknlsn/MultipleDataSetSwiftChartsExample with lollipop detail popover, and slightly simplified example below.

import Charts
import SwiftUI
import WeatherKit


struct HourWeatherStruct {
    var date: Date
    var pressure: Double
    var temperature: Double
    var windSpeed: Double
}

let hours: [HourWeatherStruct] = [
    HourWeatherStruct(date: Date(timeIntervalSinceNow: 3600),
                      pressure: 1015.0,
                      temperature: 18.2,
                      windSpeed: 6.1),
    HourWeatherStruct(date: Date(timeIntervalSinceNow: 3600 * 2),
                      pressure: 1015.3,
                      temperature: 18.2,
                      windSpeed: 8.1),
    HourWeatherStruct(date: Date(timeIntervalSinceNow: 3600 * 3),
                      pressure: 1015.9,
                      temperature: 18.2,
                      windSpeed: 9.4),
    HourWeatherStruct(date: Date(timeIntervalSinceNow: 3600 * 4),
                      pressure: 1016.3,
                      temperature: 18.2,
                      windSpeed: 5.2),
    HourWeatherStruct(date: Date(timeIntervalSinceNow: 3600 * 5),
                      pressure: 1016.3,
                      temperature: 18.2,
                      windSpeed: 12.1),
    HourWeatherStruct(date: Date(timeIntervalSinceNow: 3600 * 6),
                      pressure: 1016.3,
                      temperature: 18.2,
                      windSpeed: 11.1),
    HourWeatherStruct(date: Date(timeIntervalSinceNow: 3600 * 7),
                      pressure: 1017.3,
                      temperature: 18.2,
                      windSpeed: 10.1),
    HourWeatherStruct(date: Date(timeIntervalSinceNow: 3600 * 8),
                      pressure: 1018.3,
                      temperature: 18.2,
                      windSpeed: 11.1),
    HourWeatherStruct(date: Date(timeIntervalSinceNow: 3600 * 9),
                      pressure: 1018.3,
                      temperature: 18.2,
                      windSpeed: 9.1),
    HourWeatherStruct(date: Date(timeIntervalSinceNow: 3600 * 10),
                      pressure: 1018.3,
                      temperature: 18.2,
                      windSpeed: 8.1),
    HourWeatherStruct(date: Date(timeIntervalSinceNow: 3600 * 11),
                      pressure: 1017.3,
                      temperature: 18.2,
                      windSpeed: 19.9),
    HourWeatherStruct(date: Date(timeIntervalSinceNow: 3600 * 12),
                      pressure: 1018.3,
                      temperature: 18.2,
                      windSpeed: 7.1),
]

struct InteractiveLollipopChartMinimal: View {
    
    var body: some View {
        Chart {
            ForEach(hours, id: \.date) {
                LineMark(
                    x: .value("Date", $0.date, unit: .hour),
                    y: .value("Wind Speed", $0.windSpeed)
                )
                .foregroundStyle(by: .value("Value", "Wind"))
                
                LineMark(
                    x: .value("Date", $0.date, unit: .hour),
                    y: .value("Pressure", ($0.pressure - 1014) * 4)
                )
                .foregroundStyle(by: .value("Value", "Pressure"))
            }
            .lineStyle(StrokeStyle(lineWidth: 4.0))
            .interpolationMethod(.catmullRom)
        }
        .chartForegroundStyleScale([
            "Pressure": .purple,
            "Wind": .teal
        ])
        .chartXAxis {
            AxisMarks(position: .bottom, values: .stride(by: .hour, count: 2)) {
                _ in
                AxisTick()
                AxisGridLine()
                AxisValueLabel(format: .dateTime.hour(), centered: true)
            }
        }
        .chartYAxis {
            AxisMarks(position: .leading, values: Array(stride(from: 0, through: 24, by: 4))){
                axis in
                AxisTick()
                AxisGridLine()
                AxisValueLabel("\(1014 + (axis.index * 1))", centered: false)
            }
            AxisMarks(position: .trailing, values: Array(stride(from: 0, through: 24, by: 4))){
                axis in
                AxisTick()
                AxisGridLine()
                AxisValueLabel("\(axis.index * 4)", centered: false)
            }
        }
    }
}

struct InteractiveLollipopMinimal: View {
    
    var body: some View {
        List {
            VStack(alignment: .leading) {
                VStack(alignment: .leading) {
                    Text("Windspeed and Pressure")
                        .font(.callout)
                        .foregroundStyle(.secondary)
                    Text("\(hours.first?.date ?? Date(), format: .dateTime)")
                        .font(.title2.bold())
                }
                
                InteractiveLollipopChartMinimal()
                    .frame(height: 200)
            }
            .listRowSeparator(.hidden)
        }
        .listStyle(.plain)
        .navigationBarTitle("Interactive Lollipop", displayMode: .inline)
    }
}

Upvotes: 4

Related Questions