Skip to content

Add a Heatmap Layer

This tutorial shows how to create heatmap visualizations on your MapMetrics Android map — ideal for showing data density like earthquake activity, population, or events.

Prerequisites

Basic Heatmap

Load earthquake data and display it as a heatmap:

kotlin
import android.graphics.Color
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.MapMetricsMap
import org.maplibre.android.maps.Style
import org.maplibre.android.style.expressions.Expression
import org.maplibre.android.style.expressions.Expression.*
import org.maplibre.android.style.layers.HeatmapLayer
import org.maplibre.android.style.layers.PropertyFactory.*
import org.maplibre.android.style.sources.GeoJsonSource
import java.net.URI

class HeatmapActivity : AppCompatActivity() {

    private lateinit var mapView: MapView
    private lateinit var map: MapMetricsMap

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_map)

        mapView = findViewById(R.id.mapView)
        mapView.onCreate(savedInstanceState)
        mapView.getMapAsync { mapMetricsMap ->
            map = mapMetricsMap

            map.setStyle(
                Style.Builder().fromUri(
                    "https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY"
                )
            ) { style ->
                addHeatmap(style)
            }
        }
    }

    private fun addHeatmap(style: Style) {
        // Add GeoJSON source with earthquake data
        style.addSource(
            GeoJsonSource(
                "earthquake-source",
                URI("https://gateway.mapmetrics.org/assets/earthquakes.geojson")
            )
        )

        // Add heatmap layer
        style.addLayer(
            HeatmapLayer("heatmap-layer", "earthquake-source").withProperties(
                // Weight based on magnitude
                heatmapWeight(
                    interpolate(
                        linear(), get("mag"),
                        stop(0, 0),
                        stop(6, 1)
                    )
                ),
                // Increase intensity with zoom
                heatmapIntensity(
                    interpolate(
                        linear(), zoom(),
                        stop(0, 1),
                        stop(9, 3)
                    )
                ),
                // Color ramp from transparent to red
                heatmapColor(
                    interpolate(
                        linear(), heatmapDensity(),
                        stop(0, rgba(33f, 102f, 172f, 0f)),
                        stop(0.2, rgb(103f, 169f, 207f)),
                        stop(0.4, rgb(209f, 229f, 240f)),
                        stop(0.6, rgb(253f, 219f, 199f)),
                        stop(0.8, rgb(239f, 138f, 98f)),
                        stop(1.0, rgb(178f, 24f, 43f))
                    )
                ),
                // Increase radius with zoom
                heatmapRadius(
                    interpolate(
                        linear(), zoom(),
                        stop(0, 2),
                        stop(9, 20)
                    )
                ),
                // Fade out at high zoom
                heatmapOpacity(
                    interpolate(
                        linear(), zoom(),
                        stop(7, 1),
                        stop(9, 0)
                    )
                )
            )
        )

        // Set initial view
        map.cameraPosition = CameraPosition.Builder()
            .target(LatLng(20.0, 0.0))
            .zoom(2.0)
            .build()
    }

    override fun onStart() { super.onStart(); mapView.onStart() }
    override fun onResume() { super.onResume(); mapView.onResume() }
    override fun onPause() { super.onPause(); mapView.onPause() }
    override fun onStop() { super.onStop(); mapView.onStop() }
    override fun onDestroy() { super.onDestroy(); mapView.onDestroy() }
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        mapView.onSaveInstanceState(outState)
    }
}

Heatmap with Point Layer Transition

Show heatmap at low zoom, individual points at high zoom:

kotlin
import org.maplibre.android.style.layers.CircleLayer

private fun addHeatmapWithPoints(style: Style) {
    style.addSource(
        GeoJsonSource(
            "earthquake-source",
            URI("https://gateway.mapmetrics.org/assets/earthquakes.geojson")
        )
    )

    // Heatmap layer — visible at low zoom
    val heatmapLayer = HeatmapLayer("heatmap-layer", "earthquake-source")
        .withProperties(
            heatmapWeight(
                interpolate(linear(), get("mag"), stop(0, 0), stop(6, 1))
            ),
            heatmapColor(
                interpolate(
                    linear(), heatmapDensity(),
                    stop(0, rgba(33f, 102f, 172f, 0f)),
                    stop(0.2, rgb(103f, 169f, 207f)),
                    stop(0.4, rgb(209f, 229f, 240f)),
                    stop(0.6, rgb(253f, 219f, 199f)),
                    stop(0.8, rgb(239f, 138f, 98f)),
                    stop(1.0, rgb(178f, 24f, 43f))
                )
            ),
            heatmapRadius(
                interpolate(linear(), zoom(), stop(0, 2), stop(9, 20))
            ),
            // Fade heatmap between zoom 7-9
            heatmapOpacity(
                interpolate(linear(), zoom(), stop(7, 1), stop(9, 0))
            )
        )
    heatmapLayer.maxZoom = 9f
    style.addLayer(heatmapLayer)

    // Point layer — visible at high zoom
    val circleLayer = CircleLayer("point-layer", "earthquake-source")
        .withProperties(
            // Color by magnitude
            circleColor(
                interpolate(
                    linear(), get("mag"),
                    stop(1, color(Color.parseColor("#2DC4B2"))),
                    stop(2, color(Color.parseColor("#3BB3C3"))),
                    stop(3, color(Color.parseColor("#669EC4"))),
                    stop(4, color(Color.parseColor("#8B88B6"))),
                    stop(5, color(Color.parseColor("#A2719B"))),
                    stop(6, color(Color.parseColor("#AA5E79")))
                )
            ),
            // Size by magnitude
            circleRadius(
                interpolate(linear(), get("mag"), stop(1, 4), stop(6, 16))
            ),
            circleOpacity(
                interpolate(linear(), zoom(), stop(7, 0), stop(8, 1))
            ),
            circleStrokeColor(Color.WHITE),
            circleStrokeWidth(1f)
        )
    circleLayer.minZoom = 7f
    style.addLayer(circleLayer)
}

Heatmap Properties Reference

PropertyDescription
heatmapColorColor ramp based on heatmapDensity()
heatmapWeightWeight of each data point (often based on a property)
heatmapIntensityGlobal multiplier for density (use with zoom)
heatmapRadiusRadius of influence per point in pixels
heatmapOpacityLayer transparency (0.0 - 1.0)

Next Steps


Tip: Always pair a heatmap with a point layer transition as shown above. Heatmaps are useful for overview zoom levels (1-8), but at street level users need to see individual data points.