Swift Charts: How to show only values and labels for values in array?
Asked Answered
N

1

6

I have a Chart with WeatherKit.HourWeather objects spanning over multiple days on the x axis. However, I want to exclude the nighttime hours. It looks like I can do this with the chartXScale modifier like this:

let myDataSeperatedByHours = arrayWithAllDates.filter { ... }.sorted(...) // Array of WeatherKit.HourWeather objects filtered by isDaylight = true and sorted by date
let allDaytimeDates = myDataSeperatedByHours.map { $0.date } //only the Date objects

Chart {
      ForEach(myDataSeperatedByHours, id: \.date) { hourData in
           LineMark(
                x: .value("hour", hourData.date),
                y: .value("value", hourData.value)
           )
      }
                       

}
.chartXAxis {
        AxisMarks(position: .bottom, values: allDaytimeDates) { axisValue in
            if let date = axisValue.as(Date.self) {
                AxisValueLabel(
                    "\(Self.shortTimeFormatter.calendar.component(.hour, from: date))"
                )
            }
        }
    }
.chartXScale(domain: allDaytimeDates, type: .category)

However the Chart still displays part where there is no value. (the nighttime) I want everything removed when there is night. I've marked it green on the image below. Maybe I have to use two Charts next to each other. One for every day, but I can't believe that there's no way to do it with one Chart only.

enter image description here

I've created an example app that you can download and test here: https://github.com/Iomegan/DateChartIssue

Nb answered 28/8, 2022 at 9:35 Comment(6)
Can you show the myDataSeperatedByHours ?Direful
is myDataSeparateByHours sorted? Also might xEnd need to use myDataSeparateByHours.last instead of .first?Claudine
@Claudine I have updated the question. (first was a typo and yes it is sorted)Nb
Instead of specifying category as ScaleType, can you try using date?Amorino
with ScaleType.date I get the same error: "Fatal error: the specified scale type is incompatible with the data values and visual property."Nb
I've created an example app that you can download and test here: github.com/Iomegan/DateChartIssueNb
A
10

As per chart scale modifier documentation for domain parameter:

The possible data values along the x axis in the chart. You can define the domain with a ClosedRange for number or Date values (e.g., 0 ... 500), and with an array for categorical values (e.g., ["A", "B", "C"])

It seems for date type values this function is expecting a range but since you are specifying an array the method invocation traps.

Instead of providing the domain directly, you can provide an automatic scale domain modifying the inferred domain. To set the domain to your calculated allDaytimeDates use:

.chartXScale(domain: .automatic(dataType: Date.self) { dates in
    dates = allDaytimeDates
})

Update 1

There are multiple approaches you can try to ignore night time date scale on X-axis. The simpler and not recommended approach is to provided X-axis value in your line mark as a String instead of a Date.

The issue with specifying X-axis value as Date is you can only supply a range for the axis scale and you can't just pick multiple ranges as scale for your axis as of now and similarly you can't specify your scale to ignore certain range or values (i.e. night time). With specifying X-axis value as string you will be able to just ignore night time values:

LineMark(
    x: .value("hour", "\(hourData.date)"),
    y: .value("value", hourData.value)
)

The demerit with this approach is temprature variations as obtained from this graph is wrong as all your data points will be just separated equally regardless of their date value.

The preferred approach is to manually adjust the X-axis position for next day's data points. For your scenario you can create a DayHourWeather type with custom X-position value:

struct DayHourWeather: Plottable {
    let position: TimeInterval // Manually calculated X-axis position
    let date: Date
    let temp: Double
    let series: String // The day this data belongs to

    var primitivePlottable: TimeInterval { position }
    init?(primitivePlottable: TimeInterval) { nil }

    init(position: TimeInterval, date: Date, temp: Double, series: String) {
        self.position = position
        self.date = date
        self.temp = temp
        self.series = series
    }
}

You can customize the position data to move daytime plots closer together ignoring night time values. Then you can create DayHourWeathers from your HourWeathers:

/// assumes `hourWeathers` are filtered containing only day time data and already sorted
func getDayHourWeathers(from hourWeathers: [HourWeather]) -> [DayHourWeather] {
    let padding: TimeInterval = 10000 // distance between lat day's last data point and next day's first data point
    var translation: TimeInterval = 0 // The negetive translation required on X-axis for certain day
    var series: Int = 0 // Current day series
    var result: [DayHourWeather] = []
    result.reserveCapacity(hourWeathers.count)
    for (index, hourWeather) in hourWeathers.enumerated() {
        defer {
            result.append(
                .init(
                    position: hourWeather.date.timeIntervalSince1970 - translation,
                    date: hourWeather.date,
                    temp: hourWeather.temp,
                    series: "Day \(series + 1)"
                )
            )
        }

        guard
            index > 0,
            case let lastWeather = hourWeathers[index - 1],
            !Calendar.current.isDate(lastWeather.date, inSameDayAs: hourWeather.date)
        else { continue }

        // move next day graph to left occupying previous day's night scale
        translation = hourWeather.date.timeIntervalSince1970 - (result.last!.position + padding)
        series += 1
    }
    return result
}

Now to plot your chart you can use the newly created DayHourWeather values:

var body: some View {
    let dayWeathers = getDayHourWeathers(from: myDataSeperatedByHours)

    Chart {
        ForEach(dayWeathers, id: \.date) { hourData in
            LineMark(
                x: .value("hour", hourData.position), // custom X-axis position calculated
                y: .value("value", hourData.temp)
            )
            .foregroundStyle(by: .value("Day", hourData.series))
        }
    }
    .chartXScale(domain: dayWeathers.first!.position...dayWeathers.last!.position) // provide scale range for calculated custom X-axis positions
}

Note that with above changes your X-axis marker will display your custom X-axis positions. To change it back to the actual date label you want to display you can specify custom X-axis label:


.chartXAxis {
    AxisMarks(position: .bottom, values: dayWeathers) {
        AxisValueLabel(
            "\(Self.shortTimeFormatter.calendar.component(.hour, from: dayWeathers[$0.index].date))"
        )
    }
}

The values argument for AxisMarks only accepts an array of Plottable items, this is why confirming DayHourWeather to Plottable is needed. After above changes the chart obtained will look similar to this:

Final plot

Note that I have created a different series for each day data. Although you can combine them into a single series, I will advise against doing so as the resulting chart is misleading to viewer since you are removing part of the X-axis scale.

Amorino answered 12/9, 2022 at 7:17 Comment(4)
Thank you. The error is gone, however it only removes the values along the x axis, which aren't included in the allDaytimeDates anyway. You can clearly see that if you filter allDaytimeDates by temp values greater than 50. What I want is that also the Marks on the x axis disappear. Maybe I have to multiple Charts next to each other... :/Nb
I've updated the question and example code on Github.Nb
@Nb I have updated my answer as per my understanding of your question, let me know if I have misunderstood your question.Amorino
Thank you, I think that's how it could work for me. Thank you for the time you've spent answering this. :)Nb

© 2022 - 2025 — McMap. All rights reserved.