基于 Leaflet 的交互式 Choropleth 专题地图实战:以美国各州人口密度可视化为例
2026/9/19 19:16:03 网站建设 项目流程

基于 Leaflet 的交互式 Choropleth 专题地图实战:以美国各州人口密度可视化为例

【免费下载链接】Leaflet🍃 JavaScript library for mobile-friendly interactive maps 🇺🇦项目地址: https://gitcode.com/gh_mirrors/le/Leaflet

本教程是 Leaflet 官方示例「Interactive Choropleth Map」的深度讲解,以美国 50 个州(外加哥伦比亚特区、波多黎各)的人口密度数据为素材,逐步带你用 GeoJSON 数据、GeoJSON图层、Path样式函数与自定义Control,从零搭建一张色彩分层、可悬停高亮、点击缩放并带信息面板与图例的交互式专题地图。读完本篇文章,你将掌握 Leaflet 专题地图(Choropleth Map)的完整开发链路:数据准备、样式映射、事件交互与自定义控件,可直接迁移到任何以区域面数据为基础的可视化场景。

本教程对应的完整可运行示例位于 docs/examples/choropleth/example.html,分步示例 example-basic.html、example-color.html;示例所用 GeoJSON 数据见 docs/examples/choropleth/us-states.js。

数据准备:GeoJSON Feature 结构与数据来源

Choropleth(等值区域图)的核心思想是:用同一色系的不同深浅,表示各区域某个统计指标的高低。实现的前提是"区域边界"与"统计值"二者合一。由于本示例的数据量(各州边界坐标 + 各州密度值)并不大,最便捷的存储与展示方式就是 GeoJSON——它天然把geometry(几何边界)与properties(属性值)封装在同一个 Feature 对象里。

每个州在 us-states.js 中都是一个标准 GeoJSON Feature,结构如下:

{ "type": "Feature", "properties": { "name": "Alabama", "density": 94.65 }, "geometry": ... }

其中:

  • properties.name:州名,后续信息面板展示用;
  • properties.density:人口密度(单位:人/平方英里),是整张专题地图的"指标字段",驱动颜色分级;
  • geometry:该州的边界坐标,可能是Polygon(如 Alabama)或MultiPolygon(如 Alaska、Hawaii),Leaflet 的GeoJSON类都能正确处理。

整个文件把全部 Feature 汇总为一个FeatureCollection,并赋值给全局变量statesData,供示例页面以<script type="text/javascript" src="us-states.js"></script>方式引入。该数据由 Mike Bostock(D3 作者)贡献的州边界 GeoJSON 扩充而来,density值基于 2011 年 7 月 1 日美国人口普查局数据。

基础州界地图:把 GeoJSON 放到 Leaflet 地图上

先用一个LeafletMap初始化地图,并添加 OSM 瓦片底图,然后直接把statesData交给GeoJSON图层:

const map = new LeafletMap('map').setView([37.8, -96], 4); const tiles = new TileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }).addTo(map); new GeoJSON(statesData).addTo(map);

要点说明:

  • setView([37.8, -96], 4):把视口定位到美国本土(纬度 37.8、经度 -96),缩放级别 4 恰好能完整展示整个国家轮廓;
  • TileLayermaxZoom: 19限定了底图最大缩放级别,attribution是地图归属声明(OGC/OSM 合规要求);
  • new GeoJSON(statesData).addTo(map)会把 FeatureCollection 中的每个 Feature 转换为对应的 Leaflet 图层:Polygon/MultiPolygon转成Polygon图层,LineString转成PolylinePoint默认转成Marker

从源码看,GeoJSON.initialize会在实例化时自动调用addData解析数据(src/layer/GeoJSON.js),addData内部对FeatureCollection递归展开、逐个调用geometryToLayer完成类型分发,最后通过addLayer加入地图(src/layer/GeoJSON.js)。因此这一行代码背后实际上完成了"按几何类型分发 → 构造图层 → 挂载到 FeatureGroup"的全过程。

此步骤的独立示例见 docs/examples/choropleth/example-basic.html。

添加颜色:用密度区间驱动 fillColor 样式函数

此时所有州都渲染为默认的蓝色填充,尚无法表达密度差异。下一步用 ColorBrewer 推荐的渐变色系,定义一个"密度 → 颜色"的映射函数:

function getColor(d) { return d > 1000 ? '#800026' : d > 500 ? '#BD0026' : d > 200 ? '#E31A1C' : d > 100 ? '#FC4E2A' : d > 50 ? '#FD8D3C' : d > 20 ? '#FEB24C' : d > 10 ? '#FED976' : '#FFEDA0'; }

该函数把密度划分为 8 个区间,颜色从浅黄(低密度)渐变为深红(高密度),是典型的"连续色带 + 离散分级"配色方案。选色的依据是 ColorBrewer 这一专业的制图配色工具,其色板兼顾了色觉障碍人群的可读性。

接下来定义 GeoJSON 图层的style选项——它接收当前 Feature,返回一组 Path options:

function style(feature) { return { fillColor: getColor(feature.properties.density), weight: 2, opacity: 1, color: 'white', dashArray: '3', fillOpacity: 0.7 }; } new GeoJSON(statesData, {style: style}).addTo(map);

各样式字段含义(默认值见 src/layer/vector/Path.js):

选项本示例值Path 默认值说明
fillColorgetColor(...)返回值null(回退到color填充色,Choropleth 的核心:随密度变化
weight23描边宽度(像素)
color'white''#3388ff'描边颜色,白色让相邻州边界清晰
opacity11描边不透明度
dashArray'3'nullSVGstroke-dasharray虚线模式,'3'表示 3px 虚线,形成微妙的纹理装饰
fillOpacity0.70.2填充不透明度,半透明以便透出底图

GeoJSONstyle选项是一个函数,在数据添加时被逐个图层调用(src/layer/GeoJSON.js),其内部通过_setLayerStyle把函数计算结果交给图层的setStyle方法(src/layer/GeoJSON.js),最终由渲染器更新 SVG/Canvas 的填充与描边属性。

此步骤的独立示例见 docs/examples/choropleth/example-color.html。

添加交互:悬停高亮、指针移出复位与点击缩放

静态的彩色地图信息量已经不错,但还缺少"探索感"。下面为每个州图层绑定三种交互:

1. 悬停高亮(pointerover

function highlightFeature(e) { const layer = e.target; layer.setStyle({ weight: 5, color: '#666', dashArray: '', fillOpacity: 0.7 }); layer.bringToFront(); }
  • 通过e.target拿到被悬停的图层,调用setStyle临时改样式:描边加粗到 5px、颜色变深灰#666、取消虚线;
  • layer.bringToFront()把该图层提到最上层,避免加粗后的描边被相邻州遮挡。对应Path.bringToFront实现(src/layer/vector/Path.js),SVG/Canvas 渲染器均有对应测试覆盖(见 spec/suites/layer/vector/PathSpec.js、spec/suites/layer/vector/CanvasSpec.js)。

2. 指针移出复位(pointerout

function resetHighlight(e) { geojson.resetStyle(e.target); }

GeoJSON.resetStyle会把图层样式恢复为最初style函数定义的默认状态。其实现是:先把layer.options重建为defaultOptions的副本,再重新套用style函数(src/layer/GeoJSON.js)。需要注意的是:resetStyle要求图层在创建时已通过style选项或初始 options 记录了默认样式;若不带参数调用geojson.resetStyle(),则会对组内所有图层生效(GeoJSONSpec 中对此有专门断言,见 spec/suites/layer/GeoJSONSpec.js)。

3. 点击缩放(click

function zoomToFeature(e) { map.fitBounds(e.target.getBounds()); }

e.target.getBounds()返回该州边界的经纬度范围,map.fitBounds自动调整视口与缩放级别,把州完整居中放大。

用 onEachFeature 批量绑定事件

let geojson; // ... 其他代码 function onEachFeature(feature, layer) { layer.on({ pointerover: highlightFeature, pointerout: resetHighlight, click: zoomToFeature }); } geojson = new GeoJSON(statesData, { style: style, onEachFeature: onEachFeature }).addTo(map);

onEachFeatureGeoJSON的选项之一,在每个 Feature 被创建并应用样式之后调用,接收(feature, layer)两个参数,是"给要素挂事件、绑 Popup"的标准入口(src/layer/GeoJSON.js)。这里通过layer.on({...})一次注册三种事件。

同时,为了让resetHighlight中的geojson变量可用,必须先把geojson声明为外层变量,再在初始化语句中赋值——事件回调执行时它已经指向了图层实例。示例中使用的是let geojson+ 后置赋值,你也可以直接使用const geojson = ...并将resetHighlight定义在赋值之后(完整示例 example.html 即采用后者)。

自定义信息控件:悬停显示州名与密度

官方示例在此处刻意不用 Popup,而是选用自定义Control来展示悬停信息——优点是信息位置固定、不遮挡地图、多个州之间切换时视觉更稳定。

const info = new Control(); info.onAdd = function (map) { this._div = DomUtil.create('div', 'info'); // 创建带 "info" 类的 div this.update(); return this._div; }; // 根据传入的 feature 属性更新控件内容 info.update = function (props) { this._div.innerHTML = '<h4>US Population Density</h4>' + (props ? '<b>' + props.name + '</b><br />' + props.density + ' people / mi<sup>2</sup>' : 'Hover over a state'); }; info.addTo(map);

原理解析(对照 src/control/Control.js):

  • Control是所有地图控件的基类,position选项默认'topright'(src/control/Control.js),可取值'topleft'/'topright'/'bottomleft'/'bottomright'
  • onAdd(map)是每个自定义控件必须实现的扩展方法:创建并返回容器 DOM 元素,addTo(map)会调用它并把返回元素插入地图的对应角落容器(src/control/Control.js);
  • 自定义的update(props)方法接收 Feature 的properties,用模板字符串拼出<h4>标题与州名、密度内容;props为空时显示提示语 "Hover over a state";
  • DomUtil.create('div', 'info')创建 div 并赋予类名info(对应 src/dom/DomUtil.js 的create工厂方法)。

接着改造之前的监听器,把info.update接入交互流程:

function highlightFeature(e) { // ... info.update(layer.feature.properties); } function resetHighlight(e) { // ... info.update(); }

layer.feature是 GeoJSON 解析时挂在图层上的原始 Feature 对象(addData中通过GeoJSON.asFeature(geojson)写入,见 src/layer/GeoJSON.js),因此layer.feature.properties可以直接拿到namedensity

控件配套 CSS(.info类):

.info { padding: 6px 8px; font: 14px/16px Arial, Helvetica, sans-serif; background: white; background: rgba(255,255,255,0.8); box-shadow: 0 0 15px rgba(0,0,0,0.2); border-radius: 5px; } .info h4 { margin: 0 0 5px; color: #777; }

要点:白色背景 + 80% 透明度营造半透明浮层观感,圆角与投影增强层次感,h4标题用灰色弱化以突出数据正文。

自定义图例控件:静态区间色板

图例控件比信息控件更简单——它是静态的,不随悬停变化。代码在info类的基础上复用了图例的视觉风格,并放到右下角:

const legend = new Control({position: 'bottomright'}); legend.onAdd = function (map) { const div = DomUtil.create('div', 'info legend'), grades = [0, 10, 20, 50, 100, 200, 500, 1000]; // 遍历密度区间,为每个区间生成一个带颜色方块 + 区间标签的行 for (let i = 0; i < grades.length; i++) { div.innerHTML += '<i style="background:' + getColor(grades[i] + 1) + '"></i> ' + grades[i] + (grades[i + 1] ? '&ndash;' + grades[i + 1] + '<br>' : '+'); } return div; }; legend.addTo(map);

逻辑拆解:

  • grades = [0, 10, 20, 50, 100, 200, 500, 1000]getColor的阈值一一对应(注意getColor判断用的是>,因此取grades[i] + 1能稳定命中"大于该下界"的颜色区间);
  • 每行渲染为"色块 + 区间文本":0–1010–20、…… 最后一个区间(1000+)因为grades[i+1]不存在,拼接'+'表示"1000 及以上";
  • &ndash;是 HTML 实体(–),用于区间分隔符;
  • 色块<i>元素通过内联style="background: ..."上色,<i>在此仅作为占位元素,样式由 CSS 控制。

配套 CSS:

.legend { line-height: 18px; color: #555; } .legend i { width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.7; }

<i>被设置为 18×18 的浮动色块,opacity: 0.7与州面的fillOpacity: 0.7保持一致,避免图例颜色与地图填充产生观感偏差。

完整示例与归属声明

最终的完整实现见 docs/examples/choropleth/example.html,它把上述所有代码组装在同一个<script type="module">中,并使用 ESM 导入:

import {LeafletMap, TileLayer, Control, DomUtil, GeoJSON} from 'leaflet';

此外,完整示例还额外添加了人口数据的版权归属声明(这也是发布数据类专题地图时值得借鉴的规范做法):

map.attributionControl.addAttribution('Population data &copy; <a href="http://census.gov/">US Census Bureau</a>');

地图底图(OpenStreetMap)与数据源(US Census Bureau)分别拥有各自的归属声明,Leaflet 的AttributionControl会把它们统一渲染在地图角落。

扩展思路:把 Choropleth 配方迁移到你的业务数据

本示例的"配方"具有高度可复用性,将其推广到其他场景只需替换三处:

  1. 数据层:把你的区域边界与指标值合成为 GeoJSON Feature(properties中放入指标字段),或者用GeoJSONcoordsToLatLng选项适配非标准投影/坐标系统;
  2. 样式映射:替换getColor的分级阈值与色板(可继续借助 ColorBrewer 选取适合数据分布的颜色方案),并在style函数中引用你的指标字段;
  3. 交互与控件onEachFeature绑定事件、自定义Control展示详情与图例这两套模式,几乎可以原样照搬,只需修改info.update的 HTML 模板。

如需进一步深入,可继续阅读仓库内的相关源码与测试:GeoJSON 实现与选项、Path 样式选项、Control 基类、GeoJSON 测试用例,以及基础教程 docs/examples/geojson/index.md。

【免费下载链接】Leaflet🍃 JavaScript library for mobile-friendly interactive maps 🇺🇦项目地址: https://gitcode.com/gh_mirrors/le/Leaflet

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询