Visualize data as a heatmap
This example demonstrates the creation of a heatmap layer and a circle layer using the Mapbox Maps SDK for iOS.
After loading source data using geoJSONSource
, the createHeatmapLayer()
function sets up layer configuration including using expressions
to vary colors and intensities based on density and magnitude. It also adjusts the color, weight, intensity, and radius of the heatmap dynamically with zoom level. It then adds the layer to the map. The createCircleLayer()
function indicates earthquake magnitudes using colored circles whose sizes change with magnitude. The opacity and stroke properties of the circle layer also adjust based on the zoom level.
The visibility of the 2 layers transitions based on zoom level (between zoom levels 7
and 9
) using opacity, visualizing the data as a heat map when zoomed out and as circles on higher zooms.
This example code is part of the Maps SDK for iOS Examples App, a working iOS project available on Github. iOS developers are encouraged to run the examples app locally to interact with this example in an emulator and explore other features of the Maps SDK.
See our Run the Maps SDK for iOS Examples App tutorial for step-by-step instructions.
import UIKit
import MapboxMaps
final class ViewController: UIViewController {
private var mapView: MapView!
private var cancelables = Set<AnyCancelable>()
override func viewDidLoad() {
super.viewDidLoad()
// Center the map over the United States.
let centerCoordinate = CLLocationCoordinate2D(latitude: 50, longitude: -120)
let options = MapInitOptions(cameraOptions: CameraOptions(center: centerCoordinate, zoom: 1.0), styleURI: .dark)
// Set up map view
mapView = MapView(frame: view.bounds, mapInitOptions: options)
mapView.ornaments.options.scaleBar.visibility = .hidden
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
try! self.mapView.mapboxMap.setProjection(StyleProjection(name: .globe))
view.addSubview(mapView)
mapView.mapboxMap.onMapLoaded.observeNext { [weak self] _ in
guard let self = self else { return }
try! self.mapView.mapboxMap.setAtmosphere(Atmosphere())
self.addRuntimeLayers()
}.store(in: &cancelables)
}
func addRuntimeLayers() {
createEarthquakeSource()
createHeatmapLayer()
createCircleLayer()
}
func createEarthquakeSource() {
var earthquakeSource = GeoJSONSource(id: self.earthquakeSourceId)
earthquakeSource.data = .url(self.earthquakeURL)
earthquakeSource.generateId = true
do {
try mapView.mapboxMap.addSource(earthquakeSource)
} catch {
print("Ran into an error adding a source: \(error)")
}
}
func createHeatmapLayer() {
// Add earthquake-viz layer
var heatmapLayer = HeatmapLayer(id: self.heatmapLayerId, source: self.earthquakeSourceId)
heatmapLayer.maxZoom = 9.0
heatmapLayer.sourceLayer = self.heatmapLayerSource
heatmapLayer.heatmapColor = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.heatmapDensity)
0
"rgba(33.0, 102.0, 172.0, 0.0)"
0.2
"rgb(102.0, 169.0, 207.0)"
0.4
"rgb(209.0, 229.0, 240.0)"
0.6
"rgb(253.0, 219.0, 199.0)"
0.8
"rgb(239.0, 138.0, 98.0)"
1
"rgb(178.0, 24.0, 43.0)"
}
)
// Increase the heatmap weight based on frequency and property magnitude
heatmapLayer.heatmapWeight = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.get) {"mag"}
0
0
6
1
}
)
// Increase the heatmap color weight weight by zoom level
// heatmap-intensity is a multiplier on top of heatmap-weight
heatmapLayer.heatmapIntensity = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.zoom)
0
1
9
3
}
)
// Adjust the heatmap radius by zoom level
heatmapLayer.heatmapRadius = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.zoom)
0
2
9
20
}
)
// Transition from heatmap to circle layer by zoom level
heatmapLayer.heatmapOpacity = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.zoom)
7
1
9
0
}
)
do {
try mapView.mapboxMap.addLayer(heatmapLayer, layerPosition: .above("waterway-label"))
} catch {
print("Ran into an error adding a layer: \(error)")
}
}
func createCircleLayer() {
var circleLayerSource = GeoJSONSource(id: self.earthquakeSourceId)
circleLayerSource.data = .url(self.earthquakeURL)
circleLayerSource.generateId = true
do {
try mapView.mapboxMap.addSource(circleLayerSource)
} catch {
print("Ran into an error adding a source: \(error)")
}
// Add circle layer
var circleLayer = CircleLayer(id: self.circleLayerId, source: self.earthquakeSourceId)
// Adjust the circle layer radius by zoom level
circleLayer.circleRadius = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.zoom)
Exp(.literal) {
7
}
Exp(.interpolate) {
Exp(.linear)
Exp(.get) {"mag"}
1
1
6
4
}
Exp(.literal) {
16
}
Exp(.interpolate) {
Exp(.linear)
Exp(.get) {"mag"}
1
5
6
50
}
}
)
circleLayer.circleRadiusTransition = StyleTransition(duration: 0.5, delay: 0)
circleLayer.circleStrokeColor = .constant(StyleColor(.black))
circleLayer.circleStrokeWidth = .constant(1)
// Set circle layer color by mag level
circleLayer.circleColor = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.get) { "mag" }
1
"rgba(33.0, 102.0, 172.0, 0.0)"
1
"rgb(102.0, 169.0, 207.0)"
3
"rgb(209.0, 229.0, 240.0)"
4
"rgb(253.0, 219.0, 199.0)"
5
"rgb(239.0, 138.0, 98.0)"
6
"rgb(178.0, 24.0, 43.0)"
}
)
// Adjust the circle laye opacity by zoom level
circleLayer.circleOpacity = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.zoom)
7
0
8
1
}
)
circleLayer.circleStrokeColor = .constant(.init(UIColor.white))
circleLayer.circleStrokeWidth = .constant(0.1)
do {
try mapView.mapboxMap.addLayer(circleLayer, layerPosition: .below(self.heatmapLayerId))
} catch {
print("Ran into an error adding a layer: \(error)")
}
}
}
private extension ViewController {
var earthquakeSourceId: String { "earthquakes" }
var earthquakeLayerId: String { "earthquake-viz" }
var heatmapLayerId: String { "earthquakes-heat" }
var heatmapLayerSource: String { "earthquakes" }
var circleLayerId: String { "earthquakes-circle" }
var earthquakeURL: URL { URL(string: "https://www.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson")! }
}