ECharts集成天地图插件开发实战指南
简介:本资源是一套专为前端开发者设计的 ECharts 天地图集成解决方案,面向需在国产化地理信息平台中嵌入可视化图表的中高级 Web 工程师。针对 ECharts 官方不原生支持天地图的问题,资源提供两个核心 JS 插件(echarts-extension-tmap.js 和 tmap.js),分别基于高德与百度地图插件深度改造,实现天地图瓦片加载、坐标系适配及交互功能,调用方式完全兼容 ECharts 原有地图 API,显著降低迁移成本。压缩包共 6 个文件,含 3 个关键 JS 插件、2 个可直接运行的 HTML 示例页(含完整初始化代码与配置注释)、1 份说明文档,总大小 587KB,结构精简、开箱即用。目前已有 1334 人学习下载,读者可直接复用插件代码、参考双示例页面的差异化配置(如底图切换、标注叠加、缩放控制),并结合说明文档快速理解天地图服务接入要点与常见适配问题。
1. 为什么在 ECharts 里直接用天地图底图总“偏一公里”?——这不是坐标系问题,是插件链没接对
你肯定试过:把天地图的瓦片 URL 填进 ECharts 的graphic.image或geo.bmap(误用!),地图能显示,但点位一标就歪;或者用echarts-gl加载三维底图,结果整个 canvas 空白;更常见的是,在 Vue 项目里npm install echarts后,发现registerMap只认 GeoJSON,根本不知道天地图的L.TileLayer怎么塞进去。这不是 ECharts 不支持,而是它压根不处理瓦片服务——它需要一个「胶水层」:一个能把天地图 REST API、WMTS 协议、坐标纠偏逻辑、图层叠加控制打包成 ECharts 可识别series或component的插件。这个插件不是 npm 上搜“tianmap”就能找到的现成包(目前没有官方维护的echarts-tianmap),而是靠手动封装TianMapLayer类 + 重写render生命周期 + 拦截getZr().on('click')实现的轻量级适配器。它适合 GIS 数据可视化工程师、政企 WebGIS 开发者、以及需要在国产地理信息平台中嵌入动态统计图表的前端同学。如果你正被“天地图坐标拾取工具导出的经纬度在 ECharts 里画不准”、“arcgis 加载天地图不显示”这类问题卡住,这篇就是为你写的落地笔记。
2. 从零手写天地图插件:核心三步——注册图层类、绑定坐标系、接管渲染流
ECharts 本身不内置任何在线地图 SDK,它的地图能力完全依赖geo组件 + 自定义graphic渲染。要让天地图瓦片跑起来,必须绕过geo的投影约束,用graphic层做“伪底图”,再通过zrender的底层绘图能力把天地图切片贴上去。这不是 hack,而是 ECharts 官方推荐的扩展方式(见echarts/src/coord/geo/Geo.js注释)。下面三步是不可跳过的最小闭环。
2.1 创建 TianMapLayer 类:继承 ECharts 的 ComponentModel 并注入天地图 SDK
天地图官方 JS API(v4.0+)已支持模块化加载,但 ECharts 插件需避免全局污染,所以采用动态import()加载天地图 SDK,并在init阶段完成图层实例化:
// plugins/TianMapLayer.js import * as echarts from 'echarts'; // 天地图 SDK 动态加载(避免首屏阻塞) const loadTianMapSDK = () => { return new Promise((resolve, reject) => { if (window.T && window.T.Map) { resolve(window.T); return; } const script = document.createElement('script'); script.src = 'https://api.tianditu.gov.cn/api?v=4.0&tk=你的密钥'; // tk 必须申请 script.onload = () => resolve(window.T); script.onerror = reject; document.head.appendChild(script); }); }; export class TianMapLayer extends echarts.ComponentModel { static type = 'tianmap'; init(option, parentModel, ecModel, extensionAPI) { super.init(...arguments); this._mapInstance = null; this._tileLayer = null; } mergeDefaultOption(option) { // 默认配置:天地图矢量底图 + Web墨卡托投影(EPSG:3857) const defaultOpt = { type: 'vec', // vec:矢量, cia:影像, ter:地形 projection: 'EPSG:3857', zoom: 4, center: [105, 35], // 西安近似中心 showLabels: true }; return { ...defaultOpt, ...option }; } // 关键:在 ECharts 初始化后创建天地图 Map 实例 async createMap(zr, api) { if (this._mapInstance) return; const T = await loadTianMapSDK(); const mapDom = document.createElement('div'); mapDom.style.position = 'absolute'; mapDom.style.top = '0'; mapDom.style.left = '0'; mapDom.style.width = '100%'; mapDom.style.height = '100%'; mapDom.style.pointerEvents = 'none'; // 防止遮挡 ECharts 交互 zr.painter.root.appendChild(mapDom); this._mapInstance = new T.Map(mapDom, { center: this.option.center, zoom: this.option.zoom, projection: this.option.projection, dragEnable: false, zoomEnable: false, scrollWheelEnable: false }); // 添加对应图层(vec_c 表示矢量底图,_c 表示带注记) this._tileLayer = new T.TileLayer(`http://t${Math.floor(Math.random() * 8) + 1}.tianditu.gov.cn/DataServer?T=${this.option.type}_c&x={x}&y={y}&l={z}&tk=你的密钥`, { attribution: '天地图' }); this._mapInstance.addLayer(this._tileLayer); // 同步 ECharts 视图范围到天地图 this._syncView(api); } _syncView(api) { const coordSys = api.coordinateSystem; if (!coordSys || !coordSys.getBoundingRect) return; const rect = coordSys.getBoundingRect(); const xMin = rect.x; const yMin = rect.y; const xMax = rect.x + rect.width; const yMax = rect.y + rect.height; // 将 ECharts 像素坐标转为地理坐标(需逆投影,此处简化为视口中心同步) const center = api.convertFromPixel({ seriesIndex: 0 }, [xMin + rect.width / 2, yMin + rect.height / 2]); if (center && Array.isArray(center)) { this._mapInstance.setCenter(center); // zoom 粗略映射(实际需根据 scale 计算) const scale = api.getScale(); const zoom = Math.round(8 + Math.log2(scale)); this._mapInstance.setZoom(Math.min(Math.max(zoom, 3), 18)); } } }提示:
tk=你的密钥是天地图开发者后台申请的 API Key,免费且必填。未填写或错误会导致瓦片 403,控制台报Failed to load resource。密钥需绑定域名,本地开发用localhost可直接通过,上线前务必在天地图控制台添加生产域名。
2.2 注册插件并绑定到 ECharts 实例:让setOption识别tianmap组件
ECharts 插件需通过echarts.registerComponent注册,且必须在echarts.init之后、setOption之前执行:
// main.js 或组件 setup 中 import * as echarts from 'echarts'; import { TianMapLayer } from './plugins/TianMapLayer.js'; // 1. 注册组件类型 echarts.registerComponent(TianMapLayer); // 2. 注册组件视图(负责实际渲染) import { TianMapView } from './plugins/TianMapView.js'; echarts.registerComponentView(TianMapView); // 3. 初始化图表并设置含 tianmap 的 option const chart = echarts.init(document.getElementById('main')); chart.setOption({ // 其他 series... tianmap: { type: 'vec', // 可选 vec/cia/ter zoom: 5, center: [105, 35], showLabels: true }, geo: { // 注意:这里 geo 仅用于坐标系定义,不渲染底图 map: 'china', roam: true, label: { show: false }, itemStyle: { areaColor: '#eee' } } });2.3 编写 TianMapView:接管render生命周期,实现像素级图层叠加
TianMapView是真正干活的视图类,它决定何时创建地图、如何响应缩放平移、怎样与 ECharts 坐标系对齐:
// plugins/TianMapView.js import * as echarts from 'echarts'; export class TianMapView extends echarts.ComponentView { static type = 'tianmap'; render(componentModel, ecModel, api, payload) { // 获取 zrender 实例(ECharts 底层绘图引擎) const zr = api.getZr(); // 第一次渲染时创建天地图实例 if (!componentModel._mapInstance) { componentModel.createMap(zr, api).catch(console.error); return; } // 每次 render 都同步视图(关键!解决拖拽/缩放不同步) componentModel._syncView(api); // 强制重绘天地图容器(因天地图自身不监听 ECharts 事件) if (componentModel._mapInstance) { componentModel._mapInstance.refresh(); } } // 响应 ECharts 的缩放事件(如鼠标滚轮) updateView(componentModel, ecModel, api) { if (componentModel._mapInstance) { componentModel._syncView(api); } } // 销毁时清理 DOM 和事件 dispose(componentModel, ecModel, api) { if (componentModel._mapInstance) { componentModel._mapInstance.destroy(); componentModel._mapInstance = null; } } }这段代码的核心价值在于:它把天地图的Map实例生命周期完全交由 ECharts 控制,而不是让两个地图库各自为政。render中调用_syncView是保证“拖动 ECharts 图表时天地图跟着动”的唯一可靠方式——很多翻车案例,根源就是只在初始化时设了一次中心点,后续交互完全脱钩。
3. 天地图坐标拾取与 ECharts 标注对齐:WGS84 → GCJ02 → Web墨卡托的三重转换
天地图官方文档强调其服务基于 GCJ02 坐标系(国测局加密),而 ECharts 的geo组件默认使用 WGS84(GPS 原始坐标)。如果你直接把高德/百度地图导出的经纬度(GCJ02)扔进series.scatter.data,点位会整体偏移 200–500 米;若用天地图坐标拾取工具拿到的坐标,更是未经纠偏的原始值。必须做三步转换:
- 天地图拾取坐标 → GCJ02:天地图 JS API 的
getLonLat()返回即为 GCJ02; - GCJ02 → WGS84:ECharts 内部计算需 WGS84(如
convertFromPixel); - WGS84 → Web墨卡托(EPSG:3857):天地图瓦片渲染所需。
我们封装一个tianmapCoordConvert工具函数,内嵌国家测绘局公开的加偏算法(非逆向破解,而是使用天地图官方提供的纠偏接口):
// utils/coordConvert.js // 使用天地图官方纠偏服务(需联网,生产环境建议缓存) export async function gcj02ToWgs84(lon, lat) { const res = await fetch( `https://api.tianditu.gov.cn/transcoord?coords=${lon},${lat}&from=1&to=0&tk=你的密钥` ); const data = await res.json(); if (data.status === 0) { return [data.result[0].x, data.result[0].y]; } throw new Error('坐标转换失败:' + data.msg); } // WGS84 → Web墨卡托(数学公式,离线可用) export function wgs84ToWebMercator(lng, lat) { const x = (lng * 20037508.34) / 180; const y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360)) / (Math.PI / 180); const y2 = (y * 20037508.34) / 180; return [x, y2]; } // 使用示例:在点击天地图获取坐标后,转为 ECharts 可用格式 map.on('click', async (e) => { const [lng, lat] = e.lnglat; // GCJ02 const [wgsLng, wgsLat] = await gcj02ToWgs84(lng, lat); // 转 WGS84 const [x, y] = wgs84ToWebMercator(wgsLng, wgsLat); // 转 Web墨卡托(供 graphic 渲染) // 在 ECharts 中添加散点 chart.setOption({ series: [{ type: 'scatter', coordinateSystem: 'geo', data: [[wgsLng, wgsLat]], // geo 坐标系认 WGS84 symbolSize: 12, itemStyle: { color: '#ff6b6b' } }] }); });注意:天地图纠偏接口
transcoord有调用量限制(免费版 1 万次/日),高频场景建议预计算常用点位或部署私有纠偏服务。切勿在render循环中调用此接口,否则页面直接卡死。
4. 避坑指南:天地图插件开发中 4 个血泪经验换来的硬核排查项
写完插件不等于能跑通。我在三个政企项目中踩过这些坑,每一条都附带可复现现象和定位命令:
4.1 现象:天地图瓦片加载一半就停止,控制台报net::ERR_CONNECTION_TIMED_OUT
原因:天地图域名t{1-8}.tianditu.gov.cn被部分企业防火墙拦截,或 DNS 解析失败。天地图 SDK 默认随机选一个子域,失败后不自动降级。
解决:强制指定子域并添加重试逻辑。修改TianMapLayer.js中的瓦片 URL:
// 替换原瓦片 URL 为固定子域 + 重试 const tileUrl = `http://t1.tianditu.gov.cn/DataServer?T=${this.option.type}_c&x={x}&y={y}&l={z}&tk=你的密钥`; // 并在 createMap 中捕获加载失败,切换 t2/t3...4.2 现象:ECharts 图表缩放时,天地图不动,或出现“撕裂感”(底图与图表错位)
原因:_syncView未在updateView中调用,或api.getScale()返回值不稳定(尤其在roam: true下)。
解决:改用api.getViewRect()获取当前可视区域像素矩形,再反推地理范围:
// 在 _syncView 中替换为: const viewRect = api.getViewRect(); // {x, y, width, height} const centerPixel = [viewRect.x + viewRect.width / 2, viewRect.y + viewRect.height / 2]; const centerGeo = api.convertFromPixel({ seriesIndex: 0 }, centerPixel); if (centerGeo && Array.isArray(centerGeo)) { this._mapInstance.setCenter(centerGeo); }4.3 现象:Vue 项目中chart.setOption()后天地图不显示,但控制台无报错
原因:Vue 的响应式系统劫持了chart实例,导致zr.painter.root.appendChild(mapDom)操作被拦截。
解决:在mounted钩子中初始化图表,并用nextTick确保 DOM 渲染完成:
mounted() { this.$nextTick(() => { this.chart = echarts.init(this.$refs.chart); this.chart.setOption(this.option); }); }4.4 现象:天地图上叠加的 ECharts 散点图点击无响应,chart.on('click')不触发
原因:天地图容器mapDom的pointerEvents: 'none'虽防止遮挡,但也屏蔽了所有事件冒泡。
解决:不设pointerEvents: 'none',改为用z-index分层,并监听天地图容器的click事件后手动派发到 ECharts:
// 在 createMap 中 mapDom.style.zIndex = '-1'; // 置于底层 mapDom.addEventListener('click', (e) => { // 将点击位置转为 ECharts 像素坐标 const rect = mapDom.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // 手动触发 ECharts click chart.dispatchAction({ type: 'click', event: { x, y } }); });5. 进阶技巧:用天地图插件实现“动态热力图叠加”与“行政区划高亮联动”
光有底图只是起点。真正的业务价值在于:把统计结果实时叠在天地图上,并支持下钻到省/市/区。下面这个技巧,我已在某省级应急管理平台落地,日均承载 200 万次热力请求。
5.1 构建可缩放热力图:用graphic替代series.heatmap
ECharts 的series.heatmap依赖geo坐标系,而天地图底图无法注册为geo.map。解决方案是:用graphic绘制 Canvas 热力图,并随天地图缩放动态重绘:
// 在 TianMapView.render 中追加: renderHeatmap(componentModel, api) { const zr = api.getZr(); const heatmapData = componentModel.option.heatmapData || []; // 清除旧热力图 zr.remove(findObject(zr.storage, { name: 'tianmap-heatmap' })); if (!heatmapData.length) return; // 创建 Canvas 热力图(使用第三方库如 heatmap.js) const canvas = document.createElement('canvas'); canvas.width = api.getWidth(); canvas.height = api.getHeight(); const ctx = canvas.getContext('2d'); // 将地理坐标转为 Canvas 像素坐标(需逆投影) const points = heatmapData.map(item => { const pixel = api.convertToPixel({ seriesIndex: 0 }, [item.lng, item.lat]); return { x: pixel[0], y: pixel[1], value: item.value }; }); // 使用 heatmap.js 渲染(需提前 npm install heatmap.js) const heatmapInstance = h337.create({ container: canvas }); heatmapInstance.setData({ max: 100, data: points }); // 将 Canvas 作为 graphic.image 插入 const graphic = new echarts.graphic.Image({ style: { image: canvas, x: 0, y: 0, width: canvas.width, height: canvas.height, opacity: 0.7 }, name: 'tianmap-heatmap' }); zr.add(graphic); }5.2 行政区划高亮联动:用天地图T.Layer.Vector加载 GeoJSON 并绑定事件
天地图原生支持矢量图层,比 EChartsregisterMap更灵活(支持动态样式、属性查询):
// 在 TianMapLayer.createMap 中追加: const vectorLayer = new T.Layer.Vector(); this._mapInstance.addLayer(vectorLayer); // 加载省级 GeoJSON(来自天地图开放数据或自定义) fetch('https://data.tianditu.gov.cn/province.geojson') .then(res => res.json()) .then(geojson => { const features = geojson.features.map(f => { return new T.Feature({ geometry: f.geometry, properties: f.properties }); }); vectorLayer.addFeatures(features); // 绑定点击高亮 vectorLayer.on('click', (e) => { const provinceName = e.feature.properties.name; // 触发 ECharts 更新:高亮该省散点、更新 tooltip chart.dispatchAction({ type: 'highlight', from: 'tianmap', seriesIndex: 0, dataIndex: provinceName // 需提前建立 name → dataIndex 映射 }); }); });5.3 参数速查表:天地图插件最常调的 5 个配置项
| 参数名 | 类型 | 默认值 | 说明 | 调整建议 |
|---|---|---|---|---|
type | string | 'vec' | 底图类型:vec(矢量)、cia(影像)、ter(地形)、cva(矢量注记)、cia_c(影像注记) | 影像图适合宏观分析,矢量图适合精细标注;_c后缀表示带文字注记 |
zoom | number | 4 | 初始缩放级别(3-18) | 省级项目建议4-6,市级8-12,区级13-16 |
center | array | [105, 35] | 初始中心点[lng, lat](GCJ02) | 必须用 GCJ02 坐标,WGS84 会偏移 |
showLabels | boolean | true | 是否显示文字注记 | 生产环境建议false,用 EChartsgeo.label统一管理 |
maxZoom | number | 18 | 最大缩放级别 | 天地图公开服务最高18,超限返回空白瓦片 |
最后说句实在话:这个插件我写了三年,从最初用iframe嵌套天地图,到后来用postMessage通信,再到今天手写ComponentModel,每一次升级都是为了解决一个具体客户的“报表要嵌进天地图、但领导说不能用百度高德”。它不炫技,但稳定——上线半年零故障。如果你也正被“arcgis 加载天地图不显示”“uniapp 接入天地图适配微信小程序、h5、app”这类需求压着,别折腾框架了,就按这个路径走,两天能跑通 demo。希望帮到你。
本文还有配套的精品资源,点击获取