Skip to content

Add a Heatmap in Flutter

This tutorial shows how to create a heatmap overlay to visualize data density on your MapMetrics Flutter map.

Prerequisites

Before you begin, ensure you have:

Heatmap with Weighted Circles

Flutter doesn't have a built-in heatmap layer, but you can simulate one effectively using semi-transparent, overlapping circles:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
import 'dart:math';

class HeatmapExampleScreen extends StatefulWidget {
  @override
  _HeatmapExampleScreenState createState() => _HeatmapExampleScreenState();
}

class _HeatmapExampleScreenState extends State<HeatmapExampleScreen> {
  MapMetricsController? mapController;

  // Sample data: locations with intensity (weight)
  final List<Map<String, dynamic>> heatData = [
    {'position': LatLng(48.8584, 2.2945), 'weight': 0.9},  // Eiffel Tower
    {'position': LatLng(48.8606, 2.3376), 'weight': 0.85}, // Louvre
    {'position': LatLng(48.8530, 2.3499), 'weight': 0.8},  // Notre-Dame
    {'position': LatLng(48.8867, 2.3431), 'weight': 0.7},  // Sacré-Cœur
    {'position': LatLng(48.8738, 2.2950), 'weight': 0.75}, // Arc de Triomphe
    {'position': LatLng(48.8620, 2.3520), 'weight': 0.5},
    {'position': LatLng(48.8450, 2.3400), 'weight': 0.4},
    {'position': LatLng(48.8700, 2.3100), 'weight': 0.6},
    {'position': LatLng(48.8550, 2.3700), 'weight': 0.3},
    {'position': LatLng(48.8480, 2.3200), 'weight': 0.35},
    {'position': LatLng(48.8650, 2.3300), 'weight': 0.55},
    {'position': LatLng(48.8580, 2.3050), 'weight': 0.45},
    {'position': LatLng(48.8750, 2.3500), 'weight': 0.5},
    {'position': LatLng(48.8500, 2.2800), 'weight': 0.25},
    {'position': LatLng(48.8800, 2.3200), 'weight': 0.4},
  ];

  Color _getHeatColor(double weight) {
    // Gradient: blue (cold) → green → yellow → red (hot)
    if (weight > 0.75) return Colors.red.withOpacity(0.4);
    if (weight > 0.5) return Colors.orange.withOpacity(0.35);
    if (weight > 0.25) return Colors.yellow.withOpacity(0.3);
    return Colors.green.withOpacity(0.25);
  }

  Set<Circle> get heatCircles => heatData.asMap().entries.map((entry) {
    final data = entry.value;
    final double weight = data['weight'];
    return Circle(
      circleId: CircleId('heat_${entry.key}'),
      center: data['position'],
      radius: 300 + (weight * 500), // 300–800m radius based on weight
      strokeWidth: 0,
      fillColor: _getHeatColor(weight),
    );
  }).toSet();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Heatmap')),
      body: Stack(
        children: [
          MapMetrics(
            styleUrl: 'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
            onMapCreated: (controller) => mapController = controller,
            initialCameraPosition: CameraPosition(
              target: LatLng(48.8600, 2.3300),
              zoom: 13.0,
            ),
            circles: heatCircles,
          ),
          // Legend
          Positioned(
            bottom: 24,
            right: 16,
            child: Container(
              padding: EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.white.withOpacity(0.9),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text('Intensity',
                      style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
                  SizedBox(height: 8),
                  _legendRow(Colors.red, 'High'),
                  _legendRow(Colors.orange, 'Medium-High'),
                  _legendRow(Colors.yellow, 'Medium-Low'),
                  _legendRow(Colors.green, 'Low'),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _legendRow(Color color, String label) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 2),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Container(
            width: 14, height: 14,
            decoration: BoxDecoration(
              color: color.withOpacity(0.5),
              shape: BoxShape.circle,
            ),
          ),
          SizedBox(width: 6),
          Text(label, style: TextStyle(fontSize: 12)),
        ],
      ),
    );
  }
}

Generate Heatmap from Random Data

Create a realistic heatmap from a large dataset:

dart
List<Map<String, dynamic>> _generateHeatData(LatLng center, int count) {
  final random = Random(42);
  return List.generate(count, (_) {
    // Cluster points near the center with normal-like distribution
    final lat = center.latitude + (random.nextDouble() - 0.5) * 0.05;
    final lng = center.longitude + (random.nextDouble() - 0.5) * 0.08;
    // Weight higher for points closer to center
    final dist = sqrt(pow(lat - center.latitude, 2) + pow(lng - center.longitude, 2));
    final weight = max(0.1, 1.0 - dist * 30);

    return {
      'position': LatLng(lat, lng),
      'weight': weight,
    };
  });
}

Toggle Heatmap and Points

Switch between heatmap view and individual point markers:

dart
bool showHeatmap = true;

// In your build method:
MapMetrics(
  // ... config
  circles: showHeatmap ? heatCircles : {},
  markers: showHeatmap ? {} : pointMarkers,
)

// Toggle button:
FloatingActionButton(
  onPressed: () => setState(() => showHeatmap = !showHeatmap),
  child: Icon(showHeatmap ? Icons.location_on : Icons.blur_on),
  tooltip: showHeatmap ? 'Show Points' : 'Show Heatmap',
)

Heatmap Color Scales

ScaleColorsBest For
TrafficGreen → Yellow → RedCongestion, density
TemperatureBlue → Green → Yellow → RedWeather, temperature data
MonochromeLight blue → Dark blueClean, minimal design
ViridisPurple → Blue → Green → YellowScientific data

Next Steps


Tip: For the best visual result, use strokeWidth: 0 on heatmap circles so there are no visible borders, and use generous opacity overlap between neighboring circles.