Skip to content

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:

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


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.