mapbox-data-visualization-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Data Visualization Patterns Skill

数据可视化模式技能(Mapbox地图适用)

Comprehensive patterns for visualizing data on Mapbox maps. Covers choropleth maps, heat maps, 3D extrusions, data-driven styling, animated visualizations, and performance optimization for data-heavy applications.
本文档介绍了Mapbox地图上数据可视化的各类完整模式,包括等值区域图、热力图、3D拉伸效果、数据驱动样式、动态可视化,以及针对大数据量应用的性能优化方案。

When to Use This Skill

何时使用该技能

Use this skill when:
  • Visualizing statistical data on maps (population, sales, demographics)
  • Creating choropleth maps with color-coded regions
  • Building heat maps or clustering for density visualization
  • Adding 3D visualizations (building heights, terrain elevation)
  • Implementing data-driven styling based on properties
  • Animating time-series data
  • Working with large datasets that require optimization
在以下场景使用该技能:
  • 在地图上展示统计数据(人口、销售额、人口统计信息)
  • 创建带有颜色编码区域的等值区域图
  • 构建热力图或聚类效果以展示密度数据
  • 添加3D可视化效果(建筑高度、地形海拔)
  • 基于属性实现数据驱动样式
  • 为时间序列数据添加动画效果
  • 处理需要优化的大型数据集

Visualization Types

可视化类型

Choropleth Maps

等值区域图

Best for: Regional data (states, counties, zip codes), statistical comparisons
Pattern: Color-code polygons based on data values
javascript
map.on('load', () => {
  // Add data source (GeoJSON with properties)
  map.addSource('states', {
    type: 'geojson',
    data: 'https://example.com/states.geojson' // Features with population property
  });

  // Add fill layer with data-driven color
  map.addLayer({
    id: 'states-layer',
    type: 'fill',
    source: 'states',
    paint: {
      'fill-color': [
        'interpolate',
        ['linear'],
        ['get', 'population'],
        0,
        '#f0f9ff', // Light blue for low population
        500000,
        '#7fcdff',
        1000000,
        '#0080ff',
        5000000,
        '#0040bf', // Dark blue for high population
        10000000,
        '#001f5c'
      ],
      'fill-opacity': 0.75
    }
  });

  // Add border layer
  map.addLayer({
    id: 'states-border',
    type: 'line',
    source: 'states',
    paint: {
      'line-color': '#ffffff',
      'line-width': 1
    }
  });

  // Add hover effect with reusable popup
  const popup = new mapboxgl.Popup({
    closeButton: false,
    closeOnClick: false
  });

  map.on('mousemove', 'states-layer', (e) => {
    if (e.features.length > 0) {
      map.getCanvas().style.cursor = 'pointer';

      const feature = e.features[0];
      popup
        .setLngLat(e.lngLat)
        .setHTML(
          `
          <h3>${feature.properties.name}</h3>
          <p>Population: ${feature.properties.population.toLocaleString()}</p>
        `
        )
        .addTo(map);
    }
  });

  map.on('mouseleave', 'states-layer', () => {
    map.getCanvas().style.cursor = '';
    popup.remove();
  });
});
Color Scale Strategies:
javascript
// Linear interpolation (continuous scale)
'fill-color': [
  'interpolate',
  ['linear'],
  ['get', 'value'],
  0, '#ffffcc',
  25, '#78c679',
  50, '#31a354',
  100, '#006837'
]

// Step intervals (discrete buckets)
'fill-color': [
  'step',
  ['get', 'value'],
  '#ffffcc',  // Default color
  25, '#c7e9b4',
  50, '#7fcdbb',
  75, '#41b6c4',
  100, '#2c7fb8'
]

// Case-based (categorical data)
'fill-color': [
  'match',
  ['get', 'category'],
  'residential', '#ffd700',
  'commercial', '#ff6b6b',
  'industrial', '#4ecdc4',
  'park', '#45b7d1',
  '#cccccc'  // Default
]
适用场景: 区域数据(州、县、邮政编码区)、统计对比
实现模式: 根据数据值为多边形赋予颜色
javascript
map.on('load', () => {
  // Add data source (GeoJSON with properties)
  map.addSource('states', {
    type: 'geojson',
    data: 'https://example.com/states.geojson' // Features with population property
  });

  // Add fill layer with data-driven color
  map.addLayer({
    id: 'states-layer',
    type: 'fill',
    source: 'states',
    paint: {
      'fill-color': [
        'interpolate',
        ['linear'],
        ['get', 'population'],
        0,
        '#f0f9ff', // Light blue for low population
        500000,
        '#7fcdff',
        1000000,
        '#0080ff',
        5000000,
        '#0040bf', // Dark blue for high population
        10000000,
        '#001f5c'
      ],
      'fill-opacity': 0.75
    }
  });

  // Add border layer
  map.addLayer({
    id: 'states-border',
    type: 'line',
    source: 'states',
    paint: {
      'line-color': '#ffffff',
      'line-width': 1
    }
  });

  // Add hover effect with reusable popup
  const popup = new mapboxgl.Popup({
    closeButton: false,
    closeOnClick: false
  });

  map.on('mousemove', 'states-layer', (e) => {
    if (e.features.length > 0) {
      map.getCanvas().style.cursor = 'pointer';

      const feature = e.features[0];
      popup
        .setLngLat(e.lngLat)
        .setHTML(
          `
          <h3>${feature.properties.name}</h3>
          <p>Population: ${feature.properties.population.toLocaleString()}</p>
        `
        )
        .addTo(map);
    }
  });

  map.on('mouseleave', 'states-layer', () => {
    map.getCanvas().style.cursor = '';
    popup.remove();
  });
});
颜色比例策略:
javascript
// Linear interpolation (continuous scale)
'fill-color': [
  'interpolate',
  ['linear'],
  ['get', 'value'],
  0, '#ffffcc',
  25, '#78c679',
  50, '#31a354',
  100, '#006837'
]

// Step intervals (discrete buckets)
'fill-color': [
  'step',
  ['get', 'value'],
  '#ffffcc',  // Default color
  25, '#c7e9b4',
  50, '#7fcdbb',
  75, '#41b6c4',
  100, '#2c7fb8'
]

// Case-based (categorical data)
'fill-color': [
  'match',
  ['get', 'category'],
  'residential', '#ffd700',
  'commercial', '#ff6b6b',
  'industrial', '#4ecdc4',
  'park', '#45b7d1',
  '#cccccc'  // Default
]

Heat Maps

热力图

Best for: Point density, event locations, incident clustering
Pattern: Visualize density of points
javascript
map.on('load', () => {
  // Add data source (points)
  map.addSource('incidents', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          geometry: {
            type: 'Point',
            coordinates: [-122.4194, 37.7749]
          },
          properties: {
            intensity: 1
          }
        }
        // ... more points
      ]
    }
  });

  // Add heatmap layer
  map.addLayer({
    id: 'incidents-heat',
    type: 'heatmap',
    source: 'incidents',
    maxzoom: 15,
    paint: {
      // Increase weight based on intensity property
      'heatmap-weight': ['interpolate', ['linear'], ['get', 'intensity'], 0, 0, 6, 1],
      // Increase intensity as zoom level increases
      'heatmap-intensity': ['interpolate', ['linear'], ['zoom'], 0, 1, 15, 3],
      // Color ramp for heatmap
      'heatmap-color': [
        'interpolate',
        ['linear'],
        ['heatmap-density'],
        0,
        'rgba(33,102,172,0)',
        0.2,
        'rgb(103,169,207)',
        0.4,
        'rgb(209,229,240)',
        0.6,
        'rgb(253,219,199)',
        0.8,
        'rgb(239,138,98)',
        1,
        'rgb(178,24,43)'
      ],
      // Adjust radius by zoom level
      'heatmap-radius': ['interpolate', ['linear'], ['zoom'], 0, 2, 15, 20],
      // Decrease opacity at higher zoom levels
      'heatmap-opacity': ['interpolate', ['linear'], ['zoom'], 7, 1, 15, 0]
    }
  });

  // Add circle layer for individual points at high zoom
  map.addLayer({
    id: 'incidents-point',
    type: 'circle',
    source: 'incidents',
    minzoom: 14,
    paint: {
      'circle-radius': ['interpolate', ['linear'], ['zoom'], 14, 4, 22, 30],
      'circle-color': '#ff4444',
      'circle-opacity': 0.8,
      'circle-stroke-color': '#fff',
      'circle-stroke-width': 1
    }
  });
});
适用场景: 点密度、事件位置、事件聚类
实现模式: 展示点数据的密度
javascript
map.on('load', () => {
  // Add data source (points)
  map.addSource('incidents', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          geometry: {
            type: 'Point',
            coordinates: [-122.4194, 37.7749]
          },
          properties: {
            intensity: 1
          }
        }
        // ... more points
      ]
    }
  });

  // Add heatmap layer
  map.addLayer({
    id: 'incidents-heat',
    type: 'heatmap',
    source: 'incidents',
    maxzoom: 15,
    paint: {
      // Increase weight based on intensity property
      'heatmap-weight': ['interpolate', ['linear'], ['get', 'intensity'], 0, 0, 6, 1],
      // Increase intensity as zoom level increases
      'heatmap-intensity': ['interpolate', ['linear'], ['zoom'], 0, 1, 15, 3],
      // Color ramp for heatmap
      'heatmap-color': [
        'interpolate',
        ['linear'],
        ['heatmap-density'],
        0,
        'rgba(33,102,172,0)',
        0.2,
        'rgb(103,169,207)',
        0.4,
        'rgb(209,229,240)',
        0.6,
        'rgb(253,219,199)',
        0.8,
        'rgb(239,138,98)',
        1,
        'rgb(178,24,43)'
      ],
      // Adjust radius by zoom level
      'heatmap-radius': ['interpolate', ['linear'], ['zoom'], 0, 2, 15, 20],
      // Decrease opacity at higher zoom levels
      'heatmap-opacity': ['interpolate', ['linear'], ['zoom'], 7, 1, 15, 0]
    }
  });

  // Add circle layer for individual points at high zoom
  map.addLayer({
    id: 'incidents-point',
    type: 'circle',
    source: 'incidents',
    minzoom: 14,
    paint: {
      'circle-radius': ['interpolate', ['linear'], ['zoom'], 14, 4, 22, 30],
      'circle-color': '#ff4444',
      'circle-opacity': 0.8,
      'circle-stroke-color': '#fff',
      'circle-stroke-width': 1
    }
  });
});

Clustering (Point Density)

聚类效果(点密度)

Best for: Grouping nearby points, aggregated counts, large point datasets
Pattern: Client-side clustering for visualization
Clustering is a valuable point density visualization technique alongside heat maps. Use clustering when you want discrete grouping with exact counts rather than a continuous density visualization.
javascript
map.on('load', () => {
  // Add data source with clustering enabled
  map.addSource('locations', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        // Your point features
      ]
    },
    cluster: true,
    clusterMaxZoom: 14, // Max zoom to cluster points
    clusterRadius: 50 // Radius of each cluster (default 50)
  });

  // Clustered circles - styled by point count
  map.addLayer({
    id: 'clusters',
    type: 'circle',
    source: 'locations',
    filter: ['has', 'point_count'],
    paint: {
      // Color clusters by count (step expression)
      'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 10, '#f1f075', 30, '#f28cb1'],
      // Size clusters by count
      'circle-radius': ['step', ['get', 'point_count'], 20, 10, 30, 30, 40]
    }
  });

  // Cluster count labels
  map.addLayer({
    id: 'cluster-count',
    type: 'symbol',
    source: 'locations',
    filter: ['has', 'point_count'],
    layout: {
      'text-field': ['get', 'point_count_abbreviated'],
      'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
      'text-size': 12
    }
  });

  // Individual unclustered points
  map.addLayer({
    id: 'unclustered-point',
    type: 'circle',
    source: 'locations',
    filter: ['!', ['has', 'point_count']],
    paint: {
      'circle-color': '#11b4da',
      'circle-radius': 6,
      'circle-stroke-width': 1,
      'circle-stroke-color': '#fff'
    }
  });

  // Click handler to expand clusters
  map.on('click', 'clusters', (e) => {
    const features = map.queryRenderedFeatures(e.point, {
      layers: ['clusters']
    });
    const clusterId = features[0].properties.cluster_id;

    // Get cluster expansion zoom
    map.getSource('locations').getClusterExpansionZoom(clusterId, (err, zoom) => {
      if (err) return;

      map.easeTo({
        center: features[0].geometry.coordinates,
        zoom: zoom
      });
    });
  });

  // Change cursor on hover
  map.on('mouseenter', 'clusters', () => {
    map.getCanvas().style.cursor = 'pointer';
  });
  map.on('mouseleave', 'clusters', () => {
    map.getCanvas().style.cursor = '';
  });
});
Advanced: Custom Cluster Properties
javascript
map.addSource('locations', {
  type: 'geojson',
  data: data,
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50,
  // Calculate custom cluster properties
  clusterProperties: {
    // Sum total values
    sum: ['+', ['get', 'value']],
    // Calculate max value
    max: ['max', ['get', 'value']]
  }
});

// Use custom properties in styling
'circle-color': [
  'interpolate',
  ['linear'],
  ['get', 'sum'],
  0,
  '#51bbd6',
  100,
  '#f1f075',
  1000,
  '#f28cb1'
];
When to use clustering vs heatmaps:
Use CaseClusteringHeatmap
Visual styleDiscrete circles with countsContinuous gradient
InteractionClick to expand/zoomVisual density only
Data granularityExact counts visibleApproximate density
Best forStore locators, event listingsCrime maps, incident areas
Performance with many pointsExcellent (groups automatically)Good
User understandingClear (numbered clusters)Intuitive (heat analogy)
适用场景: 邻近点分组、聚合计数、大型点数据集
实现模式: 客户端聚类可视化
聚类是与热力图互补的点密度可视化技术。当你需要带有精确计数的离散分组而非连续密度可视化时,使用聚类效果。
javascript
map.on('load', () => {
  // Add data source with clustering enabled
  map.addSource('locations', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        // Your point features
      ]
    },
    cluster: true,
    clusterMaxZoom: 14, // Max zoom to cluster points
    clusterRadius: 50 // Radius of each cluster (default 50)
  });

  // Clustered circles - styled by point count
  map.addLayer({
    id: 'clusters',
    type: 'circle',
    source: 'locations',
    filter: ['has', 'point_count'],
    paint: {
      // Color clusters by count (step expression)
      'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 10, '#f1f075', 30, '#f28cb1'],
      // Size clusters by count
      'circle-radius': ['step', ['get', 'point_count'], 20, 10, 30, 30, 40]
    }
  });

  // Cluster count labels
  map.addLayer({
    id: 'cluster-count',
    type: 'symbol',
    source: 'locations',
    filter: ['has', 'point_count'],
    layout: {
      'text-field': ['get', 'point_count_abbreviated'],
      'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
      'text-size': 12
    }
  });

  // Individual unclustered points
  map.addLayer({
    id: 'unclustered-point',
    type: 'circle',
    source: 'locations',
    filter: ['!', ['has', 'point_count']],
    paint: {
      'circle-color': '#11b4da',
      'circle-radius': 6,
      'circle-stroke-width': 1,
      'circle-stroke-color': '#fff'
    }
  });

  // Click handler to expand clusters
  map.on('click', 'clusters', (e) => {
    const features = map.queryRenderedFeatures(e.point, {
      layers: ['clusters']
    });
    const clusterId = features[0].properties.cluster_id;

    // Get cluster expansion zoom
    map.getSource('locations').getClusterExpansionZoom(clusterId, (err, zoom) => {
      if (err) return;

      map.easeTo({
        center: features[0].geometry.coordinates,
        zoom: zoom
      });
    });
  });

  // Change cursor on hover
  map.on('mouseenter', 'clusters', () => {
    map.getCanvas().style.cursor = 'pointer';
  });
  map.on('mouseleave', 'clusters', () => {
    map.getCanvas().style.cursor = '';
  });
});
进阶:自定义聚类属性
javascript
map.addSource('locations', {
  type: 'geojson',
  data: data,
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50,
  // Calculate custom cluster properties
  clusterProperties: {
    // Sum total values
    sum: ['+', ['get', 'value']],
    // Calculate max value
    max: ['max', ['get', 'value']]
  }
});

// Use custom properties in styling
'circle-color': [
  'interpolate',
  ['linear'],
  ['get', 'sum'],
  0,
  '#51bbd6',
  100,
  '#f1f075',
  1000,
  '#f28cb1'
];
聚类 vs 热力图的适用场景对比:
适用场景聚类效果热力图
视觉风格带计数的离散圆形连续渐变效果
交互性可点击展开/缩放仅展示密度信息
数据粒度可查看精确计数近似密度值
最佳适用场景门店定位、活动列表犯罪地图、事件密集区域
大数据量性能优秀(自动分组)良好
用户理解成本清晰(带编号的聚类)直观(热力类比)

3D Extrusions

3D拉伸效果

Best for: Building heights, elevation data, volumetric representation
Pattern: Extrude polygons based on data
Note: The example below works with classic styles only (
streets-v12
,
dark-v11
,
light-v11
, etc.). The Mapbox Standard style includes 3D buildings with much greater detail by default.
javascript
map.on('load', () => {
  // Insert the layer beneath any symbol layer for proper ordering
  const layers = map.getStyle().layers;
  const labelLayerId = layers.find((layer) => layer.type === 'symbol' && layer.layout['text-field']).id;

  // Add 3D buildings from basemap
  map.addLayer(
    {
      id: 'add-3d-buildings',
      source: 'composite',
      'source-layer': 'building',
      filter: ['==', 'extrude', 'true'],
      type: 'fill-extrusion',
      minzoom: 15,
      paint: {
        'fill-extrusion-color': '#aaa',
        // Smoothly transition height on zoom
        'fill-extrusion-height': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.05, ['get', 'height']],
        'fill-extrusion-base': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.05, ['get', 'min_height']],
        'fill-extrusion-opacity': 0.6
      }
    },
    labelLayerId
  );

  // Enable pitch and bearing for 3D view
  map.setPitch(45);
  map.setBearing(-17.6);
});
Using Custom Data Source:
javascript
map.on('load', () => {
  // Add your own buildings data
  map.addSource('custom-buildings', {
    type: 'geojson',
    data: 'https://example.com/buildings.geojson'
  });

  // Add 3D buildings layer
  map.addLayer({
    id: '3d-custom-buildings',
    type: 'fill-extrusion',
    source: 'custom-buildings',
    paint: {
      // Height in meters
      'fill-extrusion-height': ['get', 'height'],
      // Base height if building on terrain
      'fill-extrusion-base': ['get', 'base_height'],
      // Color by building type or height
      'fill-extrusion-color': [
        'interpolate',
        ['linear'],
        ['get', 'height'],
        0,
        '#fafa6e',
        50,
        '#eca25b',
        100,
        '#e64a45',
        200,
        '#a63e3e'
      ],
      'fill-extrusion-opacity': 0.9
    }
  });
});
Data-Driven 3D Heights:
javascript
// Population density visualization
'fill-extrusion-height': [
  'interpolate',
  ['linear'],
  ['get', 'density'],
  0, 0,
  1000, 500,    // 1000 people/sq mi = 500m height
  10000, 5000
]

// Revenue visualization (scale for visibility)
'fill-extrusion-height': [
  '*',
  ['get', 'revenue'],
  0.001  // Scale factor
]
适用场景: 建筑高度、海拔数据、体积展示
实现模式: 根据数据拉伸多边形
注意: 以下示例仅适用于经典样式
streets-v12
dark-v11
light-v11
等)。Mapbox标准样式默认包含更精细的3D建筑效果。
javascript
map.on('load', () => {
  // Insert the layer beneath any symbol layer for proper ordering
  const layers = map.getStyle().layers;
  const labelLayerId = layers.find((layer) => layer.type === 'symbol' && layer.layout['text-field']).id;

  // Add 3D buildings from basemap
  map.addLayer(
    {
      id: 'add-3d-buildings',
      source: 'composite',
      'source-layer': 'building',
      filter: ['==', 'extrude', 'true'],
      type: 'fill-extrusion',
      minzoom: 15,
      paint: {
        'fill-extrusion-color': '#aaa',
        // Smoothly transition height on zoom
        'fill-extrusion-height': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.05, ['get', 'height']],
        'fill-extrusion-base': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.05, ['get', 'min_height']],
        'fill-extrusion-opacity': 0.6
      }
    },
    labelLayerId
  );

  // Enable pitch and bearing for 3D view
  map.setPitch(45);
  map.setBearing(-17.6);
});
使用自定义数据源:
javascript
map.on('load', () => {
  // Add your own buildings data
  map.addSource('custom-buildings', {
    type: 'geojson',
    data: 'https://example.com/buildings.geojson'
  });

   // Add 3D buildings layer
  map.addLayer({
    id: '3d-custom-buildings',
    type: 'fill-extrusion',
    source: 'custom-buildings',
    paint: {
      // Height in meters
      'fill-extrusion-height': ['get', 'height'],
      // Base height if building on terrain
      'fill-extrusion-base': ['get', 'base_height'],
      // Color by building type or height
      'fill-extrusion-color': [
        'interpolate',
        ['linear'],
        ['get', 'height'],
        0,
        '#fafa6e',
        50,
        '#eca25b',
        100,
        '#e64a45',
        200,
        '#a63e3e'
      ],
      'fill-extrusion-opacity': 0.9
    }
  });
});
数据驱动的3D高度:
javascript
// Population density visualization
'fill-extrusion-height': [
  'interpolate',
  ['linear'],
  ['get', 'density'],
  0, 0,
  1000, 500,    // 1000 people/sq mi = 500m height
  10000, 5000
]

// Revenue visualization (scale for visibility)
'fill-extrusion-height': [
  '*',
  ['get', 'revenue'],
  0.001  // Scale factor
]

Circle/Bubble Maps

圆形/气泡图

Best for: Point data with magnitude, proportional symbols
Pattern: Size circles based on data values
javascript
map.on('load', () => {
  map.addSource('earthquakes', {
    type: 'geojson',
    data: 'https://example.com/earthquakes.geojson'
  });

  // Size by magnitude, color by depth
  map.addLayer({
    id: 'earthquakes',
    type: 'circle',
    source: 'earthquakes',
    paint: {
      // Size circles by magnitude
      'circle-radius': ['interpolate', ['exponential', 2], ['get', 'mag'], 0, 2, 5, 20, 8, 100],
      // Color by depth
      'circle-color': [
        'interpolate',
        ['linear'],
        ['get', 'depth'],
        0,
        '#ffffcc',
        50,
        '#a1dab4',
        100,
        '#41b6c4',
        200,
        '#2c7fb8',
        300,
        '#253494'
      ],
      'circle-stroke-color': '#ffffff',
      'circle-stroke-width': 1,
      'circle-opacity': 0.75
    }
  });

  // Add popup on click
  map.on('click', 'earthquakes', (e) => {
    const props = e.features[0].properties;
    new mapboxgl.Popup()
      .setLngLat(e.features[0].geometry.coordinates)
      .setHTML(
        `
        <h3>Magnitude ${props.mag}</h3>
        <p>Depth: ${props.depth} km</p>
        <p>Time: ${new Date(props.time).toLocaleString()}</p>
      `
      )
      .addTo(map);
  });
});
适用场景: 带量级的点数据、比例符号
实现模式: 根据数据值调整圆形大小
javascript
map.on('load', () => {
  map.addSource('earthquakes', {
    type: 'geojson',
    data: 'https://example.com/earthquakes.geojson'
  });

  // Size by magnitude, color by depth
  map.addLayer({
    id: 'earthquakes',
    type: 'circle',
    source: 'earthquakes',
    paint: {
      // Size circles by magnitude
      'circle-radius': ['interpolate', ['exponential', 2], ['get', 'mag'], 0, 2, 5, 20, 8, 100],
      // Color by depth
      'circle-color': [
        'interpolate',
        ['linear'],
        ['get', 'depth'],
        0,
        '#ffffcc',
        50,
        '#a1dab4',
        100,
        '#41b6c4',
        200,
        '#2c7fb8',
        300,
        '#253494'
      ],
      'circle-stroke-color': '#ffffff',
      'circle-stroke-width': 1,
      'circle-opacity': 0.75
    }
  });

  // Add popup on click
  map.on('click', 'earthquakes', (e) => {
    const props = e.features[0].properties;
    new mapboxgl.Popup()
      .setLngLat(e.features[0].geometry.coordinates)
      .setHTML(
        `
        <h3>Magnitude ${props.mag}</h3>
        <p>Depth: ${props.depth} km</p>
        <p>Time: ${new Date(props.time).toLocaleString()}</p>
      `
      )
      .addTo(map);
  });
});

Line Data Visualization

线数据可视化

Best for: Routes, flows, connections, networks
Pattern: Style lines based on data
javascript
map.on('load', () => {
  map.addSource('traffic', {
    type: 'geojson',
    data: 'https://example.com/traffic.geojson'
  });

  // Traffic flow with data-driven styling
  map.addLayer({
    id: 'traffic-lines',
    type: 'line',
    source: 'traffic',
    paint: {
      // Width by traffic volume
      'line-width': ['interpolate', ['exponential', 2], ['get', 'volume'], 0, 1, 1000, 5, 10000, 15],
      // Color by speed (congestion)
      'line-color': [
        'interpolate',
        ['linear'],
        ['get', 'speed'],
        0,
        '#d73027', // Red: stopped
        15,
        '#fc8d59', // Orange: slow
        30,
        '#fee08b', // Yellow: moderate
        45,
        '#d9ef8b', // Light green: good
        60,
        '#91cf60', // Green: free flow
        75,
        '#1a9850'
      ],
      'line-opacity': 0.8
    }
  });
});
适用场景: 路线、流量、连接关系、网络
实现模式: 根据数据样式化线条
javascript
map.on('load', () => {
  map.addSource('traffic', {
    type: 'geojson',
    data: 'https://example.com/traffic.geojson'
  });

  // Traffic flow with data-driven styling
  map.addLayer({
    id: 'traffic-lines',
    type: 'line',
    source: 'traffic',
    paint: {
      // Width by traffic volume
      'line-width': ['interpolate', ['exponential', 2], ['get', 'volume'], 0, 1, 1000, 5, 10000, 15],
      // Color by speed (congestion)
      'line-color': [
        'interpolate',
        ['linear'],
        ['get', 'speed'],
        0,
        '#d73027', // Red: stopped
        15,
        '#fc8d59', // Orange: slow
        30,
        '#fee08b', // Yellow: moderate
        45,
        '#d9ef8b', // Light green: good
        60,
        '#91cf60', // Green: free flow
        75,
        '#1a9850'
      ],
      'line-opacity': 0.8
    }
  });
});

Animated Data Visualizations

动态数据可视化

Time-Series Animation

时间序列动画

Pattern: Animate data over time
javascript
let currentTime = 0;
const times = [0, 6, 12, 18, 24]; // Hours of day
let animationId;

map.on('load', () => {
  map.addSource('hourly-data', {
    type: 'geojson',
    data: getDataForTime(currentTime)
  });

  map.addLayer({
    id: 'data-layer',
    type: 'circle',
    source: 'hourly-data',
    paint: {
      'circle-radius': 8,
      'circle-color': ['get', 'color']
    }
  });

  // Animation loop
  function animate() {
    currentTime = (currentTime + 1) % times.length;

    // Update data
    map.getSource('hourly-data').setData(getDataForTime(times[currentTime]));

    // Update UI
    document.getElementById('time-display').textContent = `${times[currentTime]}:00`;

    animationId = setTimeout(animate, 1000); // Update every second
  }

  // Start animation
  document.getElementById('play-button').addEventListener('click', () => {
    if (animationId) {
      clearTimeout(animationId);
      animationId = null;
    } else {
      animate();
    }
  });
});

function getDataForTime(hour) {
  // Fetch or generate data for specific time
  return {
    type: 'FeatureCollection',
    features: data.filter((d) => d.properties.hour === hour)
  };
}
实现模式: 随时间动态展示数据
javascript
let currentTime = 0;
const times = [0, 6, 12, 18, 24]; // Hours of day
let animationId;

map.on('load', () => {
  map.addSource('hourly-data', {
    type: 'geojson',
    data: getDataForTime(currentTime)
  });

  map.addLayer({
    id: 'data-layer',
    type: 'circle',
    source: 'hourly-data',
    paint: {
      'circle-radius': 8,
      'circle-color': ['get', 'color']
    }
  });

  // Animation loop
  function animate() {
    currentTime = (currentTime + 1) % times.length;

    // Update data
    map.getSource('hourly-data').setData(getDataForTime(times[currentTime]));

    // Update UI
    document.getElementById('time-display').textContent = `${times[currentTime]}:00`;

    animationId = setTimeout(animate, 1000); // Update every second
  }

  // Start animation
  document.getElementById('play-button').addEventListener('click', () => {
    if (animationId) {
      clearTimeout(animationId);
      animationId = null;
    } else {
      animate();
    }
  });
});

function getDataForTime(hour) {
  // Fetch or generate data for specific time
  return {
    type: 'FeatureCollection',
    features: data.filter((d) => d.properties.hour === hour)
  };
}

Real-Time Data Updates

实时数据更新

Pattern: Update data from live sources
javascript
map.on('load', () => {
  map.addSource('live-data', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: []
    }
  });

  map.addLayer({
    id: 'live-points',
    type: 'circle',
    source: 'live-data',
    paint: {
      'circle-radius': 6,
      'circle-color': '#ff4444'
    }
  });

  // Poll for updates every 5 seconds
  setInterval(async () => {
    const response = await fetch('https://api.example.com/live-data');
    const data = await response.json();

    // Update source
    map.getSource('live-data').setData(data);
  }, 5000);

  // Or use WebSocket for real-time updates
  const ws = new WebSocket('wss://api.example.com/live');

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    map.getSource('live-data').setData(data);
  };
});
实现模式: 从实时数据源更新数据
javascript
map.on('load', () => {
  map.addSource('live-data', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: []
    }
  });

  map.addLayer({
    id: 'live-points',
    type: 'circle',
    source: 'live-data',
    paint: {
      'circle-radius': 6,
      'circle-color': '#ff4444'
    }
  });

  // Poll for updates every 5 seconds
  setInterval(async () => {
    const response = await fetch('https://api.example.com/live-data');
    const data = await response.json();

    // Update source
    map.getSource('live-data').setData(data);
  }, 5000);

  // Or use WebSocket for real-time updates
  const ws = new WebSocket('wss://api.example.com/live');

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    map.getSource('live-data').setData(data);
  };
});

Smooth Transitions

平滑过渡

Pattern: Animate property changes
javascript
// Smoothly transition circle sizes
function updateVisualization(newData) {
  map.getSource('data-source').setData(newData);

  // Animate circle radius
  const currentRadius = map.getPaintProperty('data-layer', 'circle-radius');
  const targetRadius = ['get', 'newSize'];

  // Use setPaintProperty with transition
  map.setPaintProperty('data-layer', 'circle-radius', targetRadius);

  // Or use expressions for smooth interpolation
  map.setPaintProperty('data-layer', 'circle-radius', ['interpolate', ['linear'], ['get', 'value'], 0, 2, 100, 20]);
}
实现模式: 动态属性的平滑过渡
javascript
// Smoothly transition circle sizes
function updateVisualization(newData) {
  map.getSource('data-source').setData(newData);

  // Animate circle radius
  const currentRadius = map.getPaintProperty('data-layer', 'circle-radius');
  const targetRadius = ['get', 'newSize'];

  // Use setPaintProperty with transition
  map.setPaintProperty('data-layer', 'circle-radius', targetRadius);

  // Or use expressions for smooth interpolation
  map.setPaintProperty('data-layer', 'circle-radius', ['interpolate', ['linear'], ['get', 'value'], 0, 2, 100, 20]);
}

Performance Optimization

性能优化

Vector Tiles vs GeoJSON

矢量瓦片 vs GeoJSON

When to use each:
Data SizeFormatReason
< 1 MBGeoJSONSimple, no processing needed
1-10 MBGeoJSON or Vector TilesConsider data update frequency
> 10 MBVector TilesBetter performance, progressive loading
Vector Tile Pattern:
javascript
map.addSource('large-dataset', {
  type: 'vector',
  tiles: ['https://example.com/tiles/{z}/{x}/{y}.mvt'],
  minzoom: 0,
  maxzoom: 14
});

map.addLayer({
  id: 'data-layer',
  type: 'fill',
  source: 'large-dataset',
  'source-layer': 'data-layer-name', // Layer name in the tileset
  paint: {
    'fill-color': ['get', 'color'],
    'fill-opacity': 0.7
  }
});
适用场景对比:
数据大小格式原因
< 1 MBGeoJSON简单,无需额外处理
1-10 MBGeoJSON 或 矢量瓦片考虑数据更新频率
> 10 MB矢量瓦片性能更优,支持渐进式加载
矢量瓦片实现模式:
javascript
map.addSource('large-dataset', {
  type: 'vector',
  tiles: ['https://example.com/tiles/{z}/{x}/{y}.mvt'],
  minzoom: 0,
  maxzoom: 14
});

map.addLayer({
  id: 'data-layer',
  type: 'fill',
  source: 'large-dataset',
  'source-layer': 'data-layer-name', // Layer name in the tileset
  paint: {
    'fill-color': ['get', 'color'],
    'fill-opacity': 0.7
  }
});

Feature State for Dynamic Styling

特征状态实现动态样式

Pattern: Update styling without modifying geometry
javascript
map.on('load', () => {
  map.addSource('states', {
    type: 'geojson',
    data: statesData,
    generateId: true // Important for feature state
  });

  map.addLayer({
    id: 'states',
    type: 'fill',
    source: 'states',
    paint: {
      'fill-color': [
        'case',
        ['boolean', ['feature-state', 'hover'], false],
        '#ff0000', // Hover color
        '#3b9ddd' // Default color
      ]
    }
  });

  let hoveredStateId = null;

  // Update feature state on hover
  map.on('mousemove', 'states', (e) => {
    if (e.features.length > 0) {
      if (hoveredStateId !== null) {
        map.setFeatureState({ source: 'states', id: hoveredStateId }, { hover: false });
      }

      hoveredStateId = e.features[0].id;

      map.setFeatureState({ source: 'states', id: hoveredStateId }, { hover: true });
    }
  });

  map.on('mouseleave', 'states', () => {
    if (hoveredStateId !== null) {
      map.setFeatureState({ source: 'states', id: hoveredStateId }, { hover: false });
    }
    hoveredStateId = null;
  });
});
实现模式: 无需修改几何数据即可更新样式
javascript
map.on('load', () => {
  map.addSource('states', {
    type: 'geojson',
    data: statesData,
    generateId: true // Important for feature state
  });

  map.addLayer({
    id: 'states',
    type: 'fill',
    source: 'states',
    paint: {
      'fill-color': [
        'case',
        ['boolean', ['feature-state', 'hover'], false],
        '#ff0000', // Hover color
        '#3b9ddd' // Default color
      ]
    }
  });

  let hoveredStateId = null;

  // Update feature state on hover
  map.on('mousemove', 'states', (e) => {
    if (e.features.length > 0) {
      if (hoveredStateId !== null) {
        map.setFeatureState({ source: 'states', id: hoveredStateId }, { hover: false });
      }

      hoveredStateId = e.features[0].id;

      map.setFeatureState({ source: 'states', id: hoveredStateId }, { hover: true });
    }
  });

  map.on('mouseleave', 'states', () => {
    if (hoveredStateId !== null) {
      map.setFeatureState({ source: 'states', id: hoveredStateId }, { hover: false });
    }
    hoveredStateId = null;
  });
});

Filtering Large Datasets

大型数据集过滤

Pattern: Filter data client-side for performance
javascript
map.on('load', () => {
  map.addSource('all-data', {
    type: 'geojson',
    data: largeDataset
  });

  map.addLayer({
    id: 'filtered-data',
    type: 'circle',
    source: 'all-data',
    filter: ['>=', ['get', 'value'], 50], // Only show values >= 50
    paint: {
      'circle-radius': 6,
      'circle-color': '#ff4444'
    }
  });

  // Update filter dynamically
  function updateFilter(minValue) {
    map.setFilter('filtered-data', ['>=', ['get', 'value'], minValue]);
  }

  // Slider for dynamic filtering
  document.getElementById('filter-slider').addEventListener('input', (e) => {
    updateFilter(parseFloat(e.target.value));
  });
});
实现模式: 客户端过滤数据以提升性能
javascript
map.on('load', () => {
  map.addSource('all-data', {
    type: 'geojson',
    data: largeDataset
  });

  map.addLayer({
    id: 'filtered-data',
    type: 'circle',
    source: 'all-data',
    filter: ['>=', ['get', 'value'], 50], // Only show values >= 50
    paint: {
      'circle-radius': 6,
      'circle-color': '#ff4444'
    }
  });

  // Update filter dynamically
  function updateFilter(minValue) {
    map.setFilter('filtered-data', ['>=', ['get', 'value'], minValue]);
  }

  // Slider for dynamic filtering
  document.getElementById('filter-slider').addEventListener('input', (e) => {
    updateFilter(parseFloat(e.target.value));
  });
});

Progressive Loading

渐进式加载

Pattern: Load data in chunks as needed
javascript
// Helper to check if feature is in bounds
function isFeatureInBounds(feature, bounds) {
  const coords = feature.geometry.coordinates;

  // Handle different geometry types
  if (feature.geometry.type === 'Point') {
    return bounds.contains(coords);
  } else if (feature.geometry.type === 'LineString') {
    return coords.some((coord) => bounds.contains(coord));
  } else if (feature.geometry.type === 'Polygon') {
    return coords[0].some((coord) => bounds.contains(coord));
  }
  return false;
}

const bounds = map.getBounds();
const visibleData = allData.features.filter((feature) => isFeatureInBounds(feature, bounds));

map.getSource('data-source').setData({
  type: 'FeatureCollection',
  features: visibleData
});

// Reload on map move with debouncing
let updateTimeout;
map.on('moveend', () => {
  clearTimeout(updateTimeout);
  updateTimeout = setTimeout(() => {
    const bounds = map.getBounds();
    const visibleData = allData.features.filter((feature) => isFeatureInBounds(feature, bounds));

    map.getSource('data-source').setData({
      type: 'FeatureCollection',
      features: visibleData
    });
  }, 150);
});
实现模式: 根据需要分块加载数据
javascript
// Helper to check if feature is in bounds
function isFeatureInBounds(feature, bounds) {
  const coords = feature.geometry.coordinates;

  // Handle different geometry types
  if (feature.geometry.type === 'Point') {
    return bounds.contains(coords);
  } else if (feature.geometry.type === 'LineString') {
    return coords.some((coord) => bounds.contains(coord));
  } else if (feature.geometry.type === 'Polygon') {
    return coords[0].some((coord) => bounds.contains(coord));
  }
  return false;
}

const bounds = map.getBounds();
const visibleData = allData.features.filter((feature) => isFeatureInBounds(feature, bounds));

map.getSource('data-source').setData({
  type: 'FeatureCollection',
  features: visibleData
});

// Reload on map move with debouncing
let updateTimeout;
map.on('moveend', () => {
  clearTimeout(updateTimeout);
  updateTimeout = setTimeout(() => {
    const bounds = map.getBounds();
    const visibleData = allData.features.filter((feature) => isFeatureInBounds(feature, bounds));

    map.getSource('data-source').setData({
      type: 'FeatureCollection',
      features: visibleData
    });
  }, 150);
});

Legends and UI Controls

图例与UI控件

Color Scale Legend

颜色比例图例

html
<div class="legend">
  <h4>Population Density</h4>
  <div class="legend-scale">
    <div class="legend-item">
      <span class="legend-color" style="background: #f0f9ff;"></span>
      <span>0-500</span>
    </div>
    <div class="legend-item">
      <span class="legend-color" style="background: #7fcdff;"></span>
      <span>500-1000</span>
    </div>
    <div class="legend-item">
      <span class="legend-color" style="background: #0080ff;"></span>
      <span>1000-5000</span>
    </div>
    <div class="legend-item">
      <span class="legend-color" style="background: #001f5c;"></span>
      <span>5000+</span>
    </div>
  </div>
</div>

<style>
  .legend {
    position: absolute;
    bottom: 30px;
    right: 10px;
    background: white;
    padding: 10px;
    border-radius: 3px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
    font-family: Arial, sans-serif;
    font-size: 12px;
  }

  .legend h4 {
    margin: 0 0 10px 0;
    font-size: 14px;
  }

  .legend-item {
    display: flex;
    align-items: center;
    margin-bottom: 5px;
  }

  .legend-color {
    width: 20px;
    height: 20px;
    margin-right: 10px;
    border: 1px solid #ccc;
  }
</style>
html
<div class="legend">
  <h4>Population Density</h4>
  <div class="legend-scale">
    <div class="legend-item">
      <span class="legend-color" style="background: #f0f9ff;"></span>
      <span>0-500</span>
    </div>
    <div class="legend-item">
      <span class="legend-color" style="background: #7fcdff;"></span>
      <span>500-1000</span>
    </div>
    <div class="legend-item">
      <span class="legend-color" style="background: #0080ff;"></span>
      <span>1000-5000</span>
    </div>
    <div class="legend-item">
      <span class="legend-color" style="background: #001f5c;"></span>
      <span>5000+</span>
    </div>
  </div>
</div>

<style>
  .legend {
    position: absolute;
    bottom: 30px;
    right: 10px;
    background: white;
    padding: 10px;
    border-radius: 3px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
    font-family: Arial, sans-serif;
    font-size: 12px;
  }

  .legend h4 {
    margin: 0 0 10px 0;
    font-size: 14px;
  }

  .legend-item {
    display: flex;
    align-items: center;
    margin-bottom: 5px;
  }

  .legend-color {
    width: 20px;
    height: 20px;
    margin-right: 10px;
    border: 1px solid #ccc;
  }
</style>

Interactive Data Inspector

交互式数据检查器

javascript
map.on('click', 'data-layer', (e) => {
  const feature = e.features[0];
  const properties = feature.properties;

  // Build properties table
  const propsTable = Object.entries(properties)
    .map(([key, value]) => `<tr><td><strong>${key}:</strong></td><td>${value}</td></tr>`)
    .join('');

  new mapboxgl.Popup()
    .setLngLat(e.lngLat)
    .setHTML(
      `
      <div style="max-width: 300px;">
        <h3>Feature Details</h3>
        <table style="width: 100%; font-size: 12px;">
          ${propsTable}
        </table>
      </div>
    `
    )
    .addTo(map);
});
javascript
map.on('click', 'data-layer', (e) => {
  const feature = e.features[0];
  const properties = feature.properties;

  // Build properties table
  const propsTable = Object.entries(properties)
    .map(([key, value]) => `<tr><td><strong>${key}:</strong></td><td>${value}</td></tr>`)
    .join('');

  new mapboxgl.Popup()
    .setLngLat(e.lngLat)
    .setHTML(
      `
      <div style="max-width: 300px;">
        <h3>Feature Details</h3>
        <table style="width: 100%; font-size: 12px;">
          ${propsTable}
        </table>
      </div>
    `
    )
    .addTo(map);
});

Best Practices

最佳实践

Color Accessibility

颜色可访问性

javascript
// Use ColorBrewer scales for accessibility
// https://colorbrewer2.org/

// Good: Sequential (single hue)
const sequentialScale = ['#f0f9ff', '#bae4ff', '#7fcdff', '#0080ff', '#001f5c'];

// Good: Diverging (two hues)
const divergingScale = ['#d73027', '#fc8d59', '#fee08b', '#d9ef8b', '#91cf60', '#1a9850'];

// Good: Qualitative (distinct categories)
const qualitativeScale = ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00'];

// Avoid: Red-green for color-blind accessibility
// Use: Blue-orange or purple-green instead
javascript
// Use ColorBrewer scales for accessibility
// https://colorbrewer2.org/

// Good: Sequential (single hue)
const sequentialScale = ['#f0f9ff', '#bae4ff', '#7fcdff', '#0080ff', '#001f5c'];

// Good: Diverging (two hues)
const divergingScale = ['#d73027', '#fc8d59', '#fee08b', '#d9ef8b', '#91cf60', '#1a9850'];

// Good: Qualitative (distinct categories)
const qualitativeScale = ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00'];

// Avoid: Red-green for color-blind accessibility
// Use: Blue-orange or purple-green instead

Data Preprocessing

数据预处理

javascript
// Calculate statistical breaks for choropleth
// Using classybrew library (npm install classybrew)
import classybrew from 'classybrew';

function calculateJenksBreaks(values, numClasses) {
  const brew = new classybrew();
  brew.setSeries(values);
  brew.setNumClasses(numClasses);
  brew.classify('jenks');
  return brew.getBreaks();
}

// Normalize data for better visualization
function normalizeData(features, property) {
  const values = features.map((f) => f.properties[property]);
  const max = Math.max(...values);
  const min = Math.min(...values);
  const range = max - min;

  // Handle case where all values are the same
  if (range === 0) {
    return features.map((feature) => ({
      ...feature,
      properties: {
        ...feature.properties,
        normalized: 0.5
      }
    }));
  }

  return features.map((feature) => ({
    ...feature,
    properties: {
      ...feature.properties,
      normalized: (feature.properties[property] - min) / range
    }
  }));
}
javascript
// Calculate statistical breaks for choropleth
// Using classybrew library (npm install classybrew)
import classybrew from 'classybrew';

function calculateJenksBreaks(values, numClasses) {
  const brew = new classybrew();
  brew.setSeries(values);
  brew.setNumClasses(numClasses);
  brew.classify('jenks');
  return brew.getBreaks();
}

// Normalize data for better visualization
function normalizeData(features, property) {
  const values = features.map((f) => f.properties[property]);
  const max = Math.max(...values);
  const min = Math.min(...values);
  const range = max - min;

  // Handle case where all values are the same
  if (range === 0) {
    return features.map((feature) => ({
      ...feature,
      properties: {
        ...feature.properties,
        normalized: 0.5
      }
    }));
  }

  return features.map((feature) => ({
    ...feature,
    properties: {
      ...feature.properties,
      normalized: (feature.properties[property] - min) / range
    }
  }));
}

Error Handling

错误处理

javascript
// Handle missing or invalid data
map.on('load', () => {
  map.addSource('data', {
    type: 'geojson',
    data: dataUrl
  });

  map.addLayer({
    id: 'data-viz',
    type: 'fill',
    source: 'data',
    paint: {
      'fill-color': [
        'case',
        ['has', 'value'], // Check if property exists
        ['interpolate', ['linear'], ['get', 'value'], 0, '#f0f0f0', 100, '#0080ff'],
        '#cccccc' // Default color for missing data
      ]
    }
  });

  // Handle source errors
  map.on('error', (e) => {
    if (e.source === 'data') {
      console.error('Failed to load data:', e);
      showNotification('Unable to load visualization data');
    }
  });
});
javascript
// Handle missing or invalid data
map.on('load', () => {
  map.addSource('data', {
    type: 'geojson',
    data: dataUrl
  });

  map.addLayer({
    id: 'data-viz',
    type: 'fill',
    source: 'data',
    paint: {
      'fill-color': [
        'case',
        ['has', 'value'], // Check if property exists
        ['interpolate', ['linear'], ['get', 'value'], 0, '#f0f0f0', 100, '#0080ff'],
        '#cccccc' // Default color for missing data
      ]
    }
  });

  // Handle source errors
  map.on('error', (e) => {
    if (e.source === 'data') {
      console.error('Failed to load data:', e);
      showNotification('Unable to load visualization data');
    }
  });
});

Common Use Cases

常见应用场景

Election Results Map

选举结果地图

javascript
map.addLayer({
  id: 'election-results',
  type: 'fill',
  source: 'districts',
  paint: {
    'fill-color': [
      'match',
      ['get', 'winner'],
      'democrat',
      '#3b82f6',
      'republican',
      '#ef4444',
      'independent',
      '#a855f7',
      '#94a3b8' // No data
    ],
    'fill-opacity': [
      'interpolate',
      ['linear'],
      ['get', 'margin'],
      0,
      0.3, // Close race: light
      20,
      0.9 // Landslide: dark
    ]
  }
});
javascript
map.addLayer({
  id: 'election-results',
  type: 'fill',
  source: 'districts',
  paint: {
    'fill-color': [
      'match',
      ['get', 'winner'],
      'democrat',
      '#3b82f6',
      'republican',
      '#ef4444',
      'independent',
      '#a855f7',
      '#94a3b8' // No data
    ],
    'fill-opacity': [
      'interpolate',
      ['linear'],
      ['get', 'margin'],
      0,
      0.3, // Close race: light
      20,
      0.9 // Landslide: dark
    ]
  }
});

COVID-19 Case Map

COVID-19病例地图

javascript
map.addLayer({
  id: 'covid-cases',
  type: 'fill',
  source: 'counties',
  paint: {
    'fill-color': [
      'step',
      ['/', ['get', 'cases'], ['get', 'population']], // Cases per capita
      '#ffffb2',
      0.001,
      '#fed976',
      0.005,
      '#feb24c',
      0.01,
      '#fd8d3c',
      0.02,
      '#fc4e2a',
      0.05,
      '#e31a1c',
      0.1,
      '#b10026'
    ]
  }
});
javascript
map.addLayer({
  id: 'covid-cases',
  type: 'fill',
  source: 'counties',
  paint: {
    'fill-color': [
      'step',
      ['/', ['get', 'cases'], ['get', 'population']], // Cases per capita
      '#ffffb2',
      0.001,
      '#fed976',
      0.005,
      '#feb24c',
      0.01,
      '#fd8d3c',
      0.02,
      '#fc4e2a',
      0.05,
      '#e31a1c',
      0.1,
      '#b10026'
    ]
  }
});

Real Estate Price Heatmap

房地产价格热力图

javascript
map.addLayer({
  id: 'real-estate',
  type: 'circle',
  source: 'properties',
  paint: {
    'circle-radius': ['interpolate', ['exponential', 2], ['get', 'price'], 100000, 5, 1000000, 20, 10000000, 50],
    'circle-color': [
      'interpolate',
      ['linear'],
      ['get', 'price_per_sqft'],
      0,
      '#ffffcc',
      200,
      '#a1dab4',
      400,
      '#41b6c4',
      600,
      '#2c7fb8',
      800,
      '#253494'
    ],
    'circle-opacity': 0.6,
    'circle-stroke-color': '#ffffff',
    'circle-stroke-width': 1
  }
});
javascript
map.addLayer({
  id: 'real-estate',
  type: 'circle',
  source: 'properties',
  paint: {
    'circle-radius': ['interpolate', ['exponential', 2], ['get', 'price'], 100000, 5, 1000000, 20, 10000000, 50],
    'circle-color': [
      'interpolate',
      ['linear'],
      ['get', 'price_per_sqft'],
      0,
      '#ffffcc',
      200,
      '#a1dab4',
      400,
      '#41b6c4',
      600,
      '#2c7fb8',
      800,
      '#253494'
    ],
    'circle-opacity': 0.6,
    'circle-stroke-color': '#ffffff',
    'circle-stroke-width': 1
  }
});

Resources

参考资源