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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
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
| Scale | Colors | Best For |
|---|---|---|
| Traffic | Green → Yellow → Red | Congestion, density |
| Temperature | Blue → Green → Yellow → Red | Weather, temperature data |
| Monochrome | Light blue → Dark blue | Clean, minimal design |
| Viridis | Purple → Blue → Green → Yellow | Scientific data |
Next Steps
- Add Clusters — Group markers instead of overlapping
- Multiple Geometries — Combine heatmap with other layers
- Draw a Circle — More about circle styling
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.