Hexagon Layer — Data Aggregation in Flutter
This tutorial shows how to create a hexagonal grid to aggregate and visualize point data — useful for density maps, analytics dashboards, and spatial analysis.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Basic Hexagon Grid
Create a hexagonal grid overlay and color cells by point count:
dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class HexagonLayerScreen extends StatefulWidget {
@override
_HexagonLayerScreenState createState() => _HexagonLayerScreenState();
}
class _HexagonLayerScreenState extends State<HexagonLayerScreen> {
MapMetricsController? mapController;
// Sample data points (e.g., reported incidents, sightings)
final List<LatLng> dataPoints = [
LatLng(48.860, 2.340), LatLng(48.861, 2.342), LatLng(48.859, 2.341),
LatLng(48.862, 2.343), LatLng(48.858, 2.339), LatLng(48.860, 2.341),
LatLng(48.855, 2.350), LatLng(48.856, 2.352), LatLng(48.854, 2.349),
LatLng(48.870, 2.330), LatLng(48.871, 2.331),
LatLng(48.845, 2.360), LatLng(48.846, 2.361), LatLng(48.847, 2.359),
LatLng(48.844, 2.362), LatLng(48.845, 2.358), LatLng(48.846, 2.360),
LatLng(48.846, 2.363), LatLng(48.843, 2.361),
LatLng(48.865, 2.320),
LatLng(48.850, 2.310), LatLng(48.851, 2.311),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Hexagon Layer')),
body: Stack(
children: [
MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.855, 2.340),
zoom: 13.0,
),
polygons: _buildHexagons(),
// Show original data points as small markers
circles: dataPoints.asMap().entries.map((e) {
return Circle(
circleId: CircleId('point_${e.key}'),
center: e.value,
radius: 30,
fillColor: Colors.black.withOpacity(0.5),
strokeWidth: 0,
);
}).toSet(),
),
// Legend
Positioned(
bottom: 16,
left: 16,
child: Card(
child: Padding(
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Density',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)),
SizedBox(height: 4),
_legendRow(Colors.green.withOpacity(0.3), '1-2 points'),
_legendRow(Colors.yellow.withOpacity(0.4), '3-4 points'),
_legendRow(Colors.orange.withOpacity(0.5), '5-6 points'),
_legendRow(Colors.red.withOpacity(0.6), '7+ points'),
],
),
),
),
),
],
),
);
}
Widget _legendRow(Color color, String label) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 1),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 16, height: 16, color: color),
SizedBox(width: 6),
Text(label, style: TextStyle(fontSize: 11)),
],
),
);
}
/// Build hexagonal polygons and color them by data density
Set<Polygon> _buildHexagons() {
final hexSize = 0.005; // Size of hexagon in degrees
final minLat = 48.840;
final maxLat = 48.875;
final minLng = 2.300;
final maxLng = 2.370;
final hexagons = <Polygon>{};
int hexIndex = 0;
for (double lat = minLat; lat < maxLat; lat += hexSize * 1.5) {
for (double lng = minLng; lng < maxLng; lng += hexSize * 1.732) {
// Offset every other row
final rowOffset =
((lat - minLat) / (hexSize * 1.5)).round() % 2 == 1
? hexSize * 0.866
: 0.0;
final centerLat = lat;
final centerLng = lng + rowOffset;
// Count points in this hexagon
final count = _countPointsInHex(centerLat, centerLng, hexSize);
if (count == 0) continue;
// Generate hexagon vertices
final vertices = _hexagonVertices(centerLat, centerLng, hexSize);
hexagons.add(
Polygon(
polygonId: PolygonId('hex_$hexIndex'),
points: vertices,
fillColor: _densityColor(count),
strokeColor: _densityColor(count).withOpacity(0.8),
strokeWidth: 1,
),
);
hexIndex++;
}
}
return hexagons;
}
List<LatLng> _hexagonVertices(double lat, double lng, double size) {
final vertices = <LatLng>[];
for (int i = 0; i < 6; i++) {
final angle = (60 * i - 30) * pi / 180;
vertices.add(LatLng(
lat + size * sin(angle),
lng + size * cos(angle),
));
}
vertices.add(vertices.first); // Close the polygon
return vertices;
}
int _countPointsInHex(double lat, double lng, double size) {
int count = 0;
for (final point in dataPoints) {
final dist = sqrt(
pow(point.latitude - lat, 2) + pow(point.longitude - lng, 2),
);
if (dist < size) count++;
}
return count;
}
Color _densityColor(int count) {
if (count <= 2) return Colors.green.withOpacity(0.3);
if (count <= 4) return Colors.yellow.withOpacity(0.4);
if (count <= 6) return Colors.orange.withOpacity(0.5);
return Colors.red.withOpacity(0.6);
}
}Interactive Hexagon with Tap Details
Tap a hexagon to see its data count:
dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class InteractiveHexScreen extends StatefulWidget {
@override
_InteractiveHexScreenState createState() => _InteractiveHexScreenState();
}
class _InteractiveHexScreenState extends State<InteractiveHexScreen> {
MapMetricsController? mapController;
String? selectedHexId;
int selectedCount = 0;
final List<LatLng> dataPoints = List.generate(50, (i) {
final random = Random(i);
return LatLng(
48.840 + random.nextDouble() * 0.035,
2.300 + random.nextDouble() * 0.070,
);
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Interactive Hexagons')),
body: Stack(
children: [
MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.855, 2.340),
zoom: 13.0,
),
polygons: _buildInteractiveHexagons(),
),
// Selected hex info
if (selectedHexId != null)
Positioned(
top: 16,
left: 16,
right: 16,
child: Card(
child: Padding(
padding: EdgeInsets.all(12),
child: Text(
'Hexagon contains $selectedCount data points',
style: TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
),
),
],
),
);
}
Set<Polygon> _buildInteractiveHexagons() {
final hexSize = 0.005;
final hexagons = <Polygon>{};
int idx = 0;
for (double lat = 48.840; lat < 48.875; lat += hexSize * 1.5) {
for (double lng = 2.300; lng < 2.370; lng += hexSize * 1.732) {
final rowOff =
((lat - 48.840) / (hexSize * 1.5)).round() % 2 == 1
? hexSize * 0.866
: 0.0;
final cLat = lat;
final cLng = lng + rowOff;
int count = 0;
for (final p in dataPoints) {
if (sqrt(pow(p.latitude - cLat, 2) + pow(p.longitude - cLng, 2)) <
hexSize) count++;
}
if (count == 0) {
idx++;
continue;
}
final hexId = 'hex_$idx';
final isSelected = hexId == selectedHexId;
final vertices = <LatLng>[];
for (int i = 0; i < 6; i++) {
final a = (60 * i - 30) * pi / 180;
vertices.add(LatLng(cLat + hexSize * sin(a), cLng + hexSize * cos(a)));
}
vertices.add(vertices.first);
hexagons.add(Polygon(
polygonId: PolygonId(hexId),
points: vertices,
fillColor: isSelected
? Colors.blue.withOpacity(0.6)
: _densityColor(count),
strokeColor: isSelected ? Colors.blue : Colors.grey,
strokeWidth: isSelected ? 3 : 1,
consumeTapEvents: true,
onTap: () {
setState(() {
selectedHexId = hexId;
selectedCount = count;
});
},
));
idx++;
}
}
return hexagons;
}
Color _densityColor(int count) {
if (count <= 2) return Colors.green.withOpacity(0.3);
if (count <= 5) return Colors.yellow.withOpacity(0.4);
return Colors.red.withOpacity(0.5);
}
}Next Steps
- Add a Heatmap — Continuous density visualization
- Add Clusters — Point clustering
- Draw GeoJSON Points — Point rendering
Tip: Hexagons are better than squares for spatial aggregation because they have uniform neighbor distances and avoid the visual bias of grid alignment. Adjust hexSize based on your zoom level and data density.