Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

Data-Driven Line Styling in Flutter

This tutorial shows how to style polylines based on data properties — useful for showing traffic speed, elevation, route type, or any varying attribute along a path.

Prerequisites

Before you begin, ensure you have:

How line styling works: a PolylineLayer takes a List<LineString> plus a single color/width that applies to every line in that layer — there's no per-line style property. To give different lines different colors, group your data by the visual style you want and render one PolylineLayer per group.

Lines Colored by Category

Style multiple route lines based on their transport type — each route already has its own color, so each becomes its own PolylineLayer:

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

class DataDrivenLinesScreen extends StatefulWidget {
  @override
  _DataDrivenLinesScreenState createState() => _DataDrivenLinesScreenState();
}

class _DataDrivenLinesScreenState extends State<DataDrivenLinesScreen> {
  MapController? mapController;

  final List<Map<String, dynamic>> routes = [
    {
      'name': 'Highway A1',
      'type': 'highway',
      'color': Colors.red,
      'width': 5,
      'points': [
        Position(2.3522, 48.8566),
        Position(2.0833, 49.2583),
        Position(2.2958, 49.8941),
        Position(3.0573, 50.6292),
      ],
    },
    {
      'name': 'Railway TGV',
      'type': 'rail',
      'color': Colors.blue,
      'width': 3,
      'points': [
        Position(2.3822, 48.8766),
        Position(2.1300, 49.2100),
        Position(2.3500, 49.8500),
        Position(3.0700, 50.6300),
      ],
    },
    {
      'name': 'Cycling Path',
      'type': 'bike',
      'color': Colors.green,
      'width': 2,
      'points': [
        Position(2.3200, 48.8400),
        Position(2.0500, 49.1800),
        Position(2.2000, 49.7500),
        Position(3.0300, 50.6000),
      ],
    },
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Data-Driven Lines')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initCenter: Position(2.5, 49.5),
              initZoom: 7.0,
              initStyle:
                  'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
            ),
            onMapCreated: (MapController controller) {
              mapController = controller;
            },
            layers: routes.map((route) {
              return PolylineLayer(
                polylines: [
                  LineString(coordinates: route['points'] as List<Position>),
                ],
                color: route['color'] as Color,
                width: route['width'] as int,
              );
            }).toList(),
          ),
          // Legend
          Positioned(
            top: 16,
            right: 16,
            child: Card(
              child: Padding(
                padding: EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Text('Transport',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                    SizedBox(height: 6),
                    _legendLine(Colors.red, 5, 'Highway'),
                    _legendLine(Colors.blue, 3, 'Railway'),
                    _legendLine(Colors.green, 2, 'Cycling'),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _legendLine(Color color, int width, String label) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 3),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Container(
            width: 24,
            height: width.toDouble(),
            color: color,
          ),
          SizedBox(width: 8),
          Text(label, style: TextStyle(fontSize: 13)),
        ],
      ),
    );
  }
}

Traffic Speed Lines

Color route segments based on traffic speed. Because PolylineLayer styles a whole layer at once, bucket the segments by the color their speed maps to, then render one PolylineLayer per bucket:

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

class TrafficSpeedScreen extends StatefulWidget {
  @override
  _TrafficSpeedScreenState createState() => _TrafficSpeedScreenState();
}

class _TrafficSpeedScreenState extends State<TrafficSpeedScreen> {
  MapController? mapController;

  // Route segments with speed data (km/h)
  final List<Map<String, dynamic>> segments = [
    {
      'from': Position(2.3522, 48.8566),
      'to': Position(2.3300, 48.8700),
      'speed': 15, // slow — traffic jam
    },
    {
      'from': Position(2.3300, 48.8700),
      'to': Position(2.3100, 48.8800),
      'speed': 35, // moderate
    },
    {
      'from': Position(2.3100, 48.8800),
      'to': Position(2.2800, 48.8950),
      'speed': 60, // fast
    },
    {
      'from': Position(2.2800, 48.8950),
      'to': Position(2.2500, 48.9100),
      'speed': 80, // very fast
    },
    {
      'from': Position(2.2500, 48.9100),
      'to': Position(2.2200, 48.9200),
      'speed': 25, // slow
    },
    {
      'from': Position(2.2200, 48.9200),
      'to': Position(2.1900, 48.9350),
      'speed': 55, // moderate-fast
    },
  ];

  /// Map speed to color (red = slow, yellow = moderate, green = fast)
  Color _speedColor(int speed) {
    if (speed < 20) return Colors.red[700]!;
    if (speed < 40) return Colors.orange;
    if (speed < 60) return Colors.yellow[700]!;
    return Colors.green;
  }

  /// Group segments by their speed-bucket color, since a PolylineLayer
  /// applies one color to every LineString it contains.
  List<PolylineLayer> _buildSpeedLayers() {
    final byColor = <Color, List<LineString>>{};
    for (final seg in segments) {
      final color = _speedColor(seg['speed'] as int);
      final line = LineString(
        coordinates: [seg['from'] as Position, seg['to'] as Position],
      );
      byColor.putIfAbsent(color, () => []).add(line);
    }

    return byColor.entries
        .map((e) => PolylineLayer(polylines: e.value, color: e.key, width: 6))
        .toList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Traffic Speed')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initCenter: Position(2.2700, 48.8900),
              initZoom: 12.0,
              initStyle:
                  'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
            ),
            onMapCreated: (MapController controller) {
              mapController = controller;
            },
            layers: _buildSpeedLayers(),
          ),
          // Speed legend
          Positioned(
            bottom: 16,
            left: 16,
            child: Card(
              child: Padding(
                padding: EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Text('Speed',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                    SizedBox(height: 4),
                    _speedRow(Colors.red[700]!, '< 20 km/h'),
                    _speedRow(Colors.orange, '20-40 km/h'),
                    _speedRow(Colors.yellow[700]!, '40-60 km/h'),
                    _speedRow(Colors.green, '> 60 km/h'),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _speedRow(Color color, String label) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 2),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Container(width: 20, height: 4, color: color),
          SizedBox(width: 6),
          Text(label, style: TextStyle(fontSize: 12)),
        ],
      ),
    );
  }
}

Line Width by Data

Vary line width based on a data property like passenger volume. Since each route already has its own volume-derived width, each route maps to its own PolylineLayer:

dart
List<PolylineLayer> _buildVolumeLines() {
  final routes = [
    {
      'name': 'Main Line',
      'volume': 50000, // daily passengers
      'points': [Position(2.35, 48.85), Position(2.20, 49.00)],
    },
    {
      'name': 'Branch A',
      'volume': 15000,
      'points': [Position(2.20, 49.00), Position(2.00, 49.10)],
    },
    {
      'name': 'Branch B',
      'volume': 5000,
      'points': [Position(2.20, 49.00), Position(2.40, 49.05)],
    },
  ];

  return routes.map((route) {
    // Map volume to width (2-10 pixels)
    final volume = route['volume'] as int;
    final width = ((volume / 50000) * 8 + 2).clamp(2, 10).toInt();

    return PolylineLayer(
      polylines: [
        LineString(coordinates: route['points'] as List<Position>),
      ],
      color: Colors.blue,
      width: width,
    );
  }).toList();
}

Data Mapping Helpers

Data TypeVisual PropertyMapping Function
SpeedColorRed (slow) -> Green (fast)
ElevationColorGreen (low) -> Red (high)
VolumeWidthThin (few) -> Thick (many)
TypeColor + PatternCategory -> fixed style
PriorityOpacityLow -> 0.3, High -> 1.0

Next Steps


Tip: For real-time data like traffic, rebuild the grouped PolylineLayer list in setState() when new speed data arrives. Split long routes into short segments so each segment can be grouped into the right color bucket based on current conditions.