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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
How line styling works: a
PolylineLayertakes aList<LineString>plus a singlecolor/widththat 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 onePolylineLayerper 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:
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:
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:
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 Type | Visual Property | Mapping Function |
|---|---|---|
| Speed | Color | Red (slow) -> Green (fast) |
| Elevation | Color | Green (low) -> Red (high) |
| Volume | Width | Thin (few) -> Thick (many) |
| Type | Color + Pattern | Category -> fixed style |
| Priority | Opacity | Low -> 0.3, High -> 1.0 |
Next Steps
- Gradient Line — Color gradient along a line
- Add a Polyline — Basic polyline drawing
- Add a GeoJSON Line — GeoJSON-based lines
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.