Using Measurements from Foundation as values in Swift Charts
In this post we are going to build a bar chart, comparing durations of nature walks in the Christchurch area. We will be using the new Swift Charts framework introduced this year and will see how to plot data of types that don't conform to Plottable
protocol by default such as Measurement<UnitDuration>
.
# Define data for the chart
Let's start by defining the data to visualize in the chart.
We declare a Walk
struct containing the title and the duration of the walk in hours. We use the Measurement type from Foundation framework with the unit type of UnitDuration to represent the duration of each walk.
struct Walk {
let title: String
let duration: Measurement<UnitDuration>
}
We store the walks to show in the chart in an array.
let walks = [
Walk(
title: "Taylors Mistake to Sumner Beach Coastal Walk",
duration: Measurement(value: 3.1, unit: .hours)
),
Walk(
title: "Bottle Lake Forest",
duration: Measurement(value: 2, unit: .hours)
),
Walk(
title: "Old Halswell Quarry Loop",
duration: Measurement(value: 0.5, unit: .hours)
),
...
]
# Build a chart to visualize walk durations
Let's define a Chart and pass it the walks
array for the data parameter. Since we know that our walk titles are unique, we can just use them as id
s, but you can also conform your data model to Identifiable
instead.
Chart(walks, id: \.title) { walk in
BarMark(
x: .value("Duration", walk.duration),
y: .value("Walk", walk.title)
)
}
Note, that because Measurement<UnitDuration>
doesn't conform to Plottable protocol by default, we will get an error Initializer 'init(x:y:width:height:stacking:)' requires that 'Measurement<UnitDuration>' conform to 'Plottable'
.
The BarkMark
initializer expects to receive a PlottableValue for x
and y
parameters. And the value type of PlottableValue
has to conform to Plottable
protocol.
We have two options to fix the error. We can either extract the value
of the measurement that is a Double
and conforms to Plottable
by default or we can extend Measurement<UnitDuration>
with Plottable
conformance.
If we simply extract the value from the measurement, we'll loose the context and won't know what units were used to create the measurement. This means that we won't be able to properly format the labels of the chart to represent the unit to the users. We could remember that we used hours
when creating the measurement, but it's not ideal. We can decide to change the data model later to store the duration in minutes, for example, or the data could be coming from somewhere else, so manually reconstructing the units is not a perfect solution. We will look into how to make Measurement<UnitDuration>
conform to Plottable
instead.
# Extend Measurement type with Plottable conformance
Measurement
is a generic type, so we need to specify the UnitType
when extending it. Since we are using Measurement<UnitDuration>
in our example, we will specify that UnitType
equals UnitDuration
, but you can adapt it to the measurements you are using in your own projects.
extension Measurement: Plottable where UnitType == UnitDuration {
public var primitivePlottable: Double {
self.converted(to: .minutes).value
}
public init?(primitivePlottable: Double) {
self.init(value: primitivePlottable, unit: .minutes)
}
}
Plottable
protocol has two requirements: primitivePlottable
property that has to return one of the primitive types such as Double
, String
or Date
and a failable initializer that creates a value from a primitive plottable type.
I decided to convert the measurement to and from minutes, but you can choose any other unit that suits your needs. It's just important to use the same unit when converting to and from the primitive value.
With the Plottable
conformance added, we can now check our chart. It works, but the labels on the x-axis are not formatted and don't show the units of measurements to the users. We are going to fix that next.
# Show formatted labels with measurement units
To customize the labels on the x-axis we will use chartXAxis(content:) modifier and reconstruct the axis marks with the values passed to us.
Chart(walks, id: \.title) { ... }
.chartXAxis {
AxisMarks { value in
AxisGridLine()
AxisValueLabel("""
\(value.as(Measurement.self)!
.converted(to: .hours),
format: .measurement(
width: .narrow,
numberFormatStyle: .number.precision(
.fractionLength(0))
)
)
""")
}
}
We first add the grid line and then reconstruct the label for a given value.
AxisValueLabel accepts a LocalizedStringKey
in the initializer, that can be constructed by interpolating a measurement and indicating its format style.
The value we receive is created using the initializer we defined in the Plottable
conformance, so in our case it is provided in minutes. But I believe it would be better to use hours for this particular chart. We can easily convert the measurement to the desired unit inside the interpolation. Here we are certain that the value is of type Measurement
, so we can force unwrap the Measurement
type cast.
I chose the narrow format and zero digits after the comma for the number style, but you can adjust these settings for your specific chart.
The final result displays formatted durations in hours on the x-axis.
You can get the full sample code for the project used in this post from our GitHub repo.