2

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?

Nicolai Henriksen
  • 1,324
  • 1
  • 13
  • 37

2 Answers2

4

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)
    }
}
jknlsn
  • 373
  • 1
  • 6
  • 2
    No, not really. I have 2 data sets and thus needs 2 sets of `LineMark` (or other mark kinds). The first needs to use the "leading" `AxisMarks`, the second needs to use the "trailing" `AxisMarks`. What you are suggesting will just give me 2 Y axis over the same data. – Nicolai Henriksen Jul 26 '22 at 17:20
  • Can you post example data? I've got a chart I am working on for my own app showing two different data sets right now, in my case temperature and wind speed. I've got two separate line charts fine, same scale though. I'm working on enabling showing pressure too, and am going to need to do some work to make the scales align and then I think this approach should work. – jknlsn Jul 26 '22 at 17:54
  • 1
    Ok, fair enough. You have two Y axis with the exact same scale and you normalize your second data set to use the same Y unit and scale as the first data set. Then you label the second Y axis in a way that makes the labels have the numbers of the wanted original second data set. This is not really what I wished/wanted, but it seems to me that your solution is the best way right now because Swift Charts does not have support for multiple (different) Y units. – Nicolai Henriksen Aug 06 '22 at 10:37
  • any luck dudes? – aaronium112 Feb 20 '23 at 17:14
0

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)
                    }
               }
            }
ChrisR
  • 9,523
  • 1
  • 8
  • 26