Data-Driven Styling
Many paint properties support data-based styling, which allows you to return different styling configurations based on the data for each feature or the map's current state, such as zoom level. MapsGL supports Mapbox style expressions (opens in a new tab) for data-driven styling, which can be used to reference data properties or evaluate custom functions to derive the style information for each feature.
Using Property Values
Sometimes your data source may provide the style information necessary for your layer and renderer type. This can often be the case when using vector (opens in a new tab) or GeoJSON (opens in a new tab) data sources where additional model data and properties are provided for each geospatial feature.
For example, our vector alerts tile set also includes information about each alert alongside its geometry data, such as the name of the alert, valid and expires times and the standard color used. If you wanted to render alerts and created your own vector tile data source, you can configure your fill.color
paint property to reference the key path that corresponds to the property containing the color value to use.
To reference a feature's property value, use the get
style expression:
{
property: ["get", "PROPERTY_KEY_PATH"]
}
For alerts vector tiles, each feature's alert-related properties are given to us in the following structure:
{
"ADVISORY": "FLOOD WARNING",
"CAT": "flood",
"COLOR": "00FF00",
"EXPIREDATE": 1652270400,
"LOCATION": "madison",
"STARTDATE": 1652112060,
"VTEC": "FL.W",
"ZONE": "ILC119"
}
For rendering our data, we want to use the value from the COLOR
property for each feature's fill color:
let alertsLayer = FillLayerDescriptor(
id: "alerts",
source: "alerts-data",
paint: .init(
fill: .init(
color: .expression(
Expression.get("COLOR")
)
)
)
)
do {
_ = try controller.addLayer(alertsLayer)
} catch {
print("Failed to add layer: \(error)")
}
Oftentimes the property you need is within a nested object in the feature's object structure, in which case you can also provide the property's key path using dot-notation. If the alert feature's data was instead provided in a structure similar to:
{
"alert": {
"name": "FLOOD WARNING",
"info": {
"category": "flood",
"color": "00FF00"
}
}
}
We could then use alert.info.color
to reference that nested property:
let alertsLayer = FillLayerDescriptor(
id: "alerts",
source: "alerts-data",
paint: .init(
fill: .init(
color: .expression(
Expression.get("alert.info.color")
)
)
)
)
do {
_ = try controller.addLayer(alertsLayer)
} catch {
print("Failed to add layer: \(error)")
}
You can use the same approach for essentially any paint property when rendering vector datasets.
Using Advanced Expressions
When you need greater control over data-driven styles or your datasets do not contain the necessary style information, you can instead provide an expression to return the style information for an individual paint property. These expressions will evaluate the data associated for each feature and use the resulting value.
An expression has the following structure:
["<operator>", <arg1>, <arg2>, ...]
For example, you start with a basic circle style layer to render current earthquake data from our Weather API. You could configure the layer and its styling as follows:
let earthquakesLayer = CircleLayerDescriptor(
id: "earthquakes",
source: "earthquakes",
paint: .init(
fill: .init(
color: .constant(.red)
),
stroke: .init(
color: .constant(.white),
thickness: .constant(3)
),
circle: .init(
radius: .constant(10)
)
)
)
do {
_ = try controller.addLayer(earthquakesLayer)
} catch {
print("Failed to add layer: \(error)")
}
But coloring each earthquake report the same color isn't a very useful data visualization since it tells the user nothing about the actual data, such as quake magnitude. A better approach would be to configure our fill
style to derive a fill color based on the magnitude of each earthquake report as provided by our weather API's earthquake endpoint:
let earthquakesLayer = CircleLayerDescriptor(
id: "earthquakes",
source: "earthquakes-data",
paint: .init(
fill: .init(
color: .expression(
Expression.match(
Expression.downcase(Expression.get("report.type")),
[
Expression.Step(value: "mini", result: UIColor.fromString("#6fb314")),
Expression.Step(value: "minor", result: UIColor.fromString("#dfcb01")),
Expression.Step(value: "light", result: UIColor.fromString("#ce8f00")),
Expression.Step(value: "moderate", result: UIColor.fromString("#ff5d01")),
Expression.Step(value: "strong", result: UIColor.fromString("#e90004")),
Expression.Step(value: "major", result: UIColor.fromString("#ce0052")),
Expression.Step(value: "great", result: UIColor.fromString("#b90285")),
Expression.Step(value: "catastrophic", result: UIColor.fromString("#f500ff")),
],
"#999999"
)
)
),
stroke: .init(
color: .constant(.white),
thickness: .constant(3)
),
circle: .init(
radius: .constant(10)
)
)
)
do {
_ = try controller.addLayer(earthquakesLayer)
} catch {
print("Failed to add layer: \(error)")
}
The above paint style configuration will result in the following output:
You can use expressions to derive values for multiple paint properties as well. So we can take the above example even further and update it to also control circle radius values based on the same magnitude data so that larger earthquakes have a greater visual impact:
let earthquakesLayer = CircleLayerDescriptor(
id: "earthquakes",
source: "earthquakes-data",
paint: .init(
fill: .init(
color: .expression(
Expression.match(
Expression.downcase(Expression.get("report.type")),
[
Expression.Step(value: "mini", result: UIColor.fromString("#6fb314")),
Expression.Step(value: "minor", result: UIColor.fromString("#dfcb01")),
Expression.Step(value: "light", result: UIColor.fromString("#ce8f00")),
Expression.Step(value: "moderate", result: UIColor.fromString("#ff5d01")),
Expression.Step(value: "strong", result: UIColor.fromString("#e90004")),
Expression.Step(value: "major", result: UIColor.fromString("#ce0052")),
Expression.Step(value: "great", result: UIColor.fromString("#b90285")),
Expression.Step(value: "catastrophic", result: UIColor.fromString("#f500ff")),
],
"#999999"
)
)
),
stroke: .init(
color: .constant(.white),
thickness: .constant(3)
),
circle: .init(
radius: .expression(
Expression.match(
Expression.downcase(Expression.get("report.type")),
[
Expression.Step(value: "minor", result: 8),
Expression.Step(value: "light", result: 9),
Expression.Step(value: "moderate", result: 10),
Expression.Step(value: "strong", result: 12),
Expression.Step(value: "major", result: 14),
Expression.Step(value: "great", result: 17),
Expression.Step(value: "catastrophic", result: 20),
],
5
)
)
)
)
)
do {
_ = try controller.addLayer(earthquakesLayer)
} catch {
print("Failed to add layer: \(error)")
}
Controlling both the color and size based on feature data will result in the following output:
Refer to the list of supported style properties and their expected return types for each render style listed above when using data-driven styling.
Note, however, that some paint properties are evaluated before data is rendered, meaning they cannot be updated once the initial styles have been calculated and data buffers created.
Styling Weather Data
Since the above example is using data provided by one of our built-in weather layers, it could have also been added to the map using addWeatherLayer
without a data source reference while still providing the same custom style overrides provided in the above examples:
do {
try controller.addWeatherLayer(config: {
var config = WeatherService.Earthquakes(service: controller.service)
// override `config.layer.paint` properties...
return config
}())
} catch {
print("Failed to add layer: \(error)")
}
Refer to our supported weather layers and weather layer styling documentation for more details and examples when working with weather layers within the MapsGL SDK.