Skip to content

使用 ECharts 地图时你应该了解的知识

本篇文章是科普向,非实战指南,详细的使用请对照官网配置项手册:geoseries-map

2018 年 3 月,百度将 ECharts 项目捐赠给 Apache 基金会,ECharts 成为了 Apache 基金会孵化级项目,同时也是首个由百度贡献给国际顶级基金会的开源项目。

2021 年 1 月 26 日晚,Apache 基金会正式官宣Apache ECharts顺利通过孵化阶段,晋升为 Apache 顶级项目。

到了 2023 年的今天,ECharts 的第五个大版本了已经发布两年了。从 v4 到 v5,ECharts 有了很多的新特性,本篇主要介绍地图相关的使用。

结合升级指南,v5 版本有以下的变动,使用的时候需要注意:

  • v5 移除了内置的 GeoJSON
  • action 名变更
    • mapToggleSelect ➡️ toggleSelect
    • mapSelect ➡️ select
    • mapUnSelect ➡️ unselect
  • 事件名变更
    • mapselectchanged ➡️ selectchanged
    • mapselected ➡️ selected
    • mapunselected ➡️ unselected
  • 选项 series.mapType 已经不推荐使用。使用 series.map 代替。
  • 选项 series.mapLocation 已经不推荐使用。

01. 地图的基石——GeoJSON

GeoJSON是用于描述各种地理区域数据的一种格式。它是一种国际通用的规范,《GeoJSON 规范》发布于 2016 年。

用 JavaScript 的概念来讲,GeoJSON 就是一个 JSON 对象,它可以通过符合规范的键值对来描述地理信息。

一个有效的 GeoJSON 大概长这样:

json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "coordinates": [116.43063202970814, 39.96344762877294],
        "type": "Point"
      }
    },
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "coordinates": [
          [116.46519108511751, 39.88763321805527],
          [121.48769570803233, 31.26687775248952]
        ],
        "type": "LineString"
      }
    },
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "coordinates": [
          [
            [112.85356201413333, 43.37087847681994],
            [112.85356201413333, 37.914976897592496],
            [119.75011160256912, 37.914976897592496],
            [119.75011160256912, 43.37087847681994],
            [112.85356201413333, 43.37087847681994]
          ]
        ],
        "type": "Polygon"
      }
    }
  ]
}
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "coordinates": [116.43063202970814, 39.96344762877294],
        "type": "Point"
      }
    },
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "coordinates": [
          [116.46519108511751, 39.88763321805527],
          [121.48769570803233, 31.26687775248952]
        ],
        "type": "LineString"
      }
    },
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "coordinates": [
          [
            [112.85356201413333, 43.37087847681994],
            [112.85356201413333, 37.914976897592496],
            [119.75011160256912, 37.914976897592496],
            [119.75011160256912, 43.37087847681994],
            [112.85356201413333, 43.37087847681994]
          ]
        ],
        "type": "Polygon"
      }
    }
  ]
}

我们可以在GeoJSON.io中查看效果,地图上深灰色的部分就是上面的 GeoJSON 示例所表达的信息,它包含一个点 Point、一个面 Polygon 和一条线 LineString

img

GeoJSON 规定,一个 GeoJSON Object 中必然有 type 属性,它有固定的几个取值:FeatureFeatureCollection 和其他 geometry type(几何类型)

img

不同的 type 取值,所需的必要成员也不同:

  • 取值FeatureCollection时,必须有成员:features
  • 取值Feature时,必须有成员:geometry
  • 取值几何类型时,必须有成员:coordinates

从上面示例的 GeoJSON 中可以看出,不同类型的层级结构大概是这样的:FeatureCollection -> Feature -> geometry type

GeoJSON 除了必要的成员外,还可以自定义成员或者在 properties 中添加自定义属性,以配合所使用的解析 GeoJSON 的工具。比如 ECharts 会默认读取 Feature 对象下的 properties.name 作为地域的中文名。

当然把自定义属性放在其他地方也可以,只要必须的属性在就行。比如 ECharts 的 GeoJSON,在与 type同级放了个 id

json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": "710000",
      "properties": {
        "id": "710000",
        "cp": [
          121.509062,
          24.044332
        ],
        "name": "台湾",
        "childNum": 6
      }
    }
  ]
}
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": "710000",
      "properties": {
        "id": "710000",
        "cp": [
          121.509062,
          24.044332
        ],
        "name": "台湾",
        "childNum": 6
      }
    }
  ]
}

GeoJSON 不光可以应用在 ECharts 绘制地图,还可以在各种地图插件中使用,比如高德地图中,可以通过Loca.GeoJSONSource加载自定义 GeoJSON 数据源。

02. 中国地图 GeoJSON

2.1 下载中国地图的 GeoJSON

前言中提到,ECharts v5 已经移除了内置的 GeoJSON,所以 ECharts v5 已经不能开箱即用地显示中国地图了。

我推荐三种方式,可以很方便地找到中国地图相关的 GeoJSON:

方式一:下载ECharts v4.x源码,在 map/json/ 下可以找到

img

方式二:下载最新版本的 ECharts 源码,虽然 v5 移除了内置的 GeoJSON,但是在 test/data/map/json/ 下还藏了一份。和 v4.x 不同的是,它还多了一个叫 china-new.json 的 GeoJSON,我在项目中用的就是这一份 GeoJSON,后面我会讲解其中的区别。

img

方式三:在DataV.GeoAtlas 地理小工具上下载最新的 GeoJSON

img

你也可以去 GitHub 上下载,很多仓库备份了中国地图相关的 GeoJSON,这里贴一个官方推荐的第三方资源:echarts-maps

2.2 对比一下中国地图 GeoJSON

通过以上三种方式,就中国地图而言,我们实际上得到了四份中国地图的 GeoJSON,为了方便区分,我重新命名下:

  • china.json(by ECharts v4.x),命名为:china-v4.json
  • china.json(by ECharts v5.x),命名为:china-v5.json
  • china-new.json(by ECharts v5.x),命名为:china-new-v5.json
  • china.json(by DataV.GeoAtlas),命名为:china-datav.json

其中china-v4.jsonchina-v5.json 是一模一样的。

从文件大小看,china-v5.jsonchina-datav.json 是有明显差异的,前者只有后者的十分之一大小:

img

通过对比具体内容发现,大小差异除了因为 china-datav.jsonproperties 中塞了更多的拓展属性外,最大的原因是 coordinates 的差异。

拿北京市举例,china-v5.json 中,coordinates 的值,是一堆乱码一样的字符串,而 china-datav.jsoncoordinates 的值虽然是正经的坐标数值,但是光一个北京市的坐标数据,格式化后就占了979行。

img

img

为什么同一区域的坐标数据,二者如此不同?

如果翻阅 ECharts 源码就可以发现,这是因为 ECharts 在注册地图解析 GeoJSON(parseGeoJSON)时,如果识别到 GeoJSON 是被压缩的(GeoJSON.UTF8Encoding === true),会先行遍历解码的操作。

这种压缩后的 GeoJSON 是 ECharts 专用的,如果其他工具需要用到,需要事先自行进行解码操作。

核心解码代码如下:

js
// src/coord/geo/parseGeoJson.ts

function decodeRing(
  coordinate: string,
  encodeOffsets: number[],
  encodeScale: number
): number[][] {
  const result = []
  let prevX = encodeOffsets[0]
  let prevY = encodeOffsets[1]

  for (let i = 0; i < coordinate.length; i += 2) {
    let x = coordinate.charCodeAt(i) - 64
    let y = coordinate.charCodeAt(i + 1) - 64
    // ZigZag decoding
    x = (x >> 1) ^ -(x & 1)
    y = (y >> 1) ^ -(y & 1)
    // Delta deocding
    x += prevX
    y += prevY

    prevX = x
    prevY = y
    // Dequantize
    result.push([x / encodeScale, y / encodeScale])
  }

  return result
}
// src/coord/geo/parseGeoJson.ts

function decodeRing(
  coordinate: string,
  encodeOffsets: number[],
  encodeScale: number
): number[][] {
  const result = []
  let prevX = encodeOffsets[0]
  let prevY = encodeOffsets[1]

  for (let i = 0; i < coordinate.length; i += 2) {
    let x = coordinate.charCodeAt(i) - 64
    let y = coordinate.charCodeAt(i + 1) - 64
    // ZigZag decoding
    x = (x >> 1) ^ -(x & 1)
    y = (y >> 1) ^ -(y & 1)
    // Delta deocding
    x += prevX
    y += prevY

    prevX = x
    prevY = y
    // Dequantize
    result.push([x / encodeScale, y / encodeScale])
  }

  return result
}

测试解压北京市的坐标数据,结果和 ECharts GeoJSON 的坐标大体相同,只是小数点会有一些差异:

img

除了 ECharts 和 DataV.GeoAtlas 的 GeoJSON 有差异,还可以看到 v5 的china-new.json要比 v4 和 v5 的china.json大了一丶丶。这涉及到南海诸岛相关的显示和配置的差异,后面会仔细讲解。

警告

南海诸岛历来都是我国领土的组成部分,所以不要想着去隐藏南海诸岛!中华人民共和国的主权和领土完整神圣不可侵犯!作为技术人员,任何情况下去绘制中国地图,都不能缺失任何领土!

另外,ECharts 还提供了的 JS 版的 GeoJSON(v4.x 在 map/js/ 下,v5.x 在 test/data/map/js/ 下),多了一个自动注册的功能,注册名称与文件同名。我不太喜欢用,想要按需加载,得手动创建 <script> 标签,并且注册地图的名称不能修改。

03. geoseries-map的区别

通过查阅官网文档,会发现,有两种方式显示地图,一种是通过geo,一种是通过series-map

它俩有啥区别?我们应该用哪个?

  • geo 是一个地理坐标系组件,它所绘制出来的地图,本质上是一个地图模样的坐标系。它没有 data 属性,无法直接给地图上每个区域绑定额外的数据。既然它是坐标系,那么在坐标系的基础上,可以 series 填充其他图形,比如在地图上绘制散点图。
  • series-map 和其他类型(柱状图、折线图......)的 series 一样,都是指定了一个 type,然后用数据去驱动展示图形。默认情况下,它会自己生成内部专用的geo组件。

它们可以一起使用,以达到更加复杂的效果。series-map 可以使用用 series-map.geoIndex 指定一个 geo 组件。这样的话,series-map 和其他series(例如散点图)就可以共享一个 geo 组件了。并且,geo 组件的颜色也可以被这个 series-map 控制,从而用 visualMap 来更改。

通俗点讲,geo 是一个坐标系,逼格更高,可以作用于所有 series。而 series-map 只是 series 的一种。series-map 在不指定公用的 geo 组件的情况下,默认会自己生成一个自己专用的。

两者一起用的话,series-map 在不指定 geo 组件时,会出现两个叠加的地图,可以通过设置两个地图不同样式实现地图外边缘发光的效果。而普通的需求(比如只需要展示地图热力图),只用 series-map 就够了。

地图外边缘发光效果示例:

js
const option = {
  backgroundColor: 'transparent',
  geo: {
    map: 'china', // 使用中国地图
    roam: false, // 不允许缩放和拖动
    itemStyle: {
      borderColor: '#a18a3a', // 边框颜色
      borderWidth: 0.5, // 边框宽度
      shadowColor: '#a18a3a', // 阴影颜色
      shadowBlur: 100 // 阴影大小
    },
    emphasis: {
      itemStyle: {
        areaColor: '#2a333d' // 高亮区域颜色
      },
      label: {
        show: false
      }
    }
  },
  series: [
    {
      type: 'map',
      map: 'china',
      zlevel: 1,
      nameProperty: 'id',
      nameMap,
      roam: false,
      select: {
        disabled: true
      },
      itemStyle: {
        areaColor: '#2F4858',
        borderColor: '#a18a3a',
        borderWidth: 1
      },
      emphasis: {
        itemStyle: {
          show: false,
          areaColor: null
        },
        label: {
          show: false
        }
      },
      data: [
        {
          name: '南海诸岛',
          value: 0,
          label: { show: false },
          itemStyle: {
            opacity: 0
          },
          tooltip: {
            extraCssText: 'opacity: 0;'
          }
        }
      ]
    }
  ]
}
const option = {
  backgroundColor: 'transparent',
  geo: {
    map: 'china', // 使用中国地图
    roam: false, // 不允许缩放和拖动
    itemStyle: {
      borderColor: '#a18a3a', // 边框颜色
      borderWidth: 0.5, // 边框宽度
      shadowColor: '#a18a3a', // 阴影颜色
      shadowBlur: 100 // 阴影大小
    },
    emphasis: {
      itemStyle: {
        areaColor: '#2a333d' // 高亮区域颜色
      },
      label: {
        show: false
      }
    }
  },
  series: [
    {
      type: 'map',
      map: 'china',
      zlevel: 1,
      nameProperty: 'id',
      nameMap,
      roam: false,
      select: {
        disabled: true
      },
      itemStyle: {
        areaColor: '#2F4858',
        borderColor: '#a18a3a',
        borderWidth: 1
      },
      emphasis: {
        itemStyle: {
          show: false,
          areaColor: null
        },
        label: {
          show: false
        }
      },
      data: [
        {
          name: '南海诸岛',
          value: 0,
          label: { show: false },
          itemStyle: {
            opacity: 0
          },
          tooltip: {
            extraCssText: 'opacity: 0;'
          }
        }
      ]
    }
  ]
}

04. 关于南海诸岛

当注册的地图名称为 china 时,在 src/coord/geo/fix/nanhai.ts 中专门针对南海做了特殊处理,会自动在右下角追加一个简略的南海缩略图。

img

下面有一份简单的示例代码:

img

在其他条件不变时,通过修改 url,来切换不同的 GeoJSON 数据源,看看显示的效果:

  • china-v5.json

img

  • china-new-v5.json

img

  • china-datav.json

img

嗯?我们中好像出现了叛徒,说好的简略的南海缩略图呢,怎么 china-new-v5.json 你的缩略图这么别致?不光有名称,还带有岛屿?

这是因为,china-new-v5.json 直接在 GeoJSON 中添加了南海诸岛缩略图:

img

再回过头来看看上面上面三种 GeoJSON 的图,它们各有各的特点:

  • china-v5.json:陆地地图在画布中占比大看着舒服,南海领海缩略图很简陋,看不到南海岛屿
  • china-new-v5.json:陆地地图在画布中占比大看着舒服,南海领海缩略图够详细,能看道南海岛屿
  • china-datav.json:陆地地图只占了一半多点,有大面积留白,看着不太舒服

这么一对比,china-new-v5.json 明显更占优势,显示的效果更好,且自带南海诸岛缩略图,不需要非得设置注册名称是china

如果我们去查看百度的产品对于 ECharts 的运用,比如百度指数,就可以发现,官方实战用的应该也是 china-new-v5.json。(哦,应该叫前官方 👻,官网的地图 demo都已经是美国的形状了)

img

05. 地图热力图

假设我们有一份地图区域数据,那么结合 visualMap,就可以实现热力图的效果:

js
const data = [
  {
    name: '上海',
    value: 9000
  },
  {
    name: '江苏',
    value: 8000
  },
  {
    name: '青海',
    value: 600
  }
]
async function init() {
  const url = './geo-jsons/china-new-v5.json'
  const res = await fetch(url)
  chinaGeoJson = await res.json()

  echarts.registerMap('china', chinaGeoJson)
  const myChart = echarts.init(document.getElementById('chart'))

  myChart.setOption({
    tooltip: {
      trigger: 'item'
    },
    visualMap: {
      type: 'piecewise',
      max: 10000,
      min: 0,
      text: ['高', '低'],
      calculable: true
    },
    series: [
      {
        name: '测试指标',
        type: 'map',
        map: 'china',
        data
      }
    ]
  })
}

init()
const data = [
  {
    name: '上海',
    value: 9000
  },
  {
    name: '江苏',
    value: 8000
  },
  {
    name: '青海',
    value: 600
  }
]
async function init() {
  const url = './geo-jsons/china-new-v5.json'
  const res = await fetch(url)
  chinaGeoJson = await res.json()

  echarts.registerMap('china', chinaGeoJson)
  const myChart = echarts.init(document.getElementById('chart'))

  myChart.setOption({
    tooltip: {
      trigger: 'item'
    },
    visualMap: {
      type: 'piecewise',
      max: 10000,
      min: 0,
      text: ['高', '低'],
      calculable: true
    },
    series: [
      {
        name: '测试指标',
        type: 'map',
        map: 'china',
        data
      }
    ]
  })
}

init()

img

从代码中可以发现,data 中的数据,只有 namevalue,没什么特殊的。它之所以会被自动绑定到对应的区域中,是因为 series-map.nameProperty 默认为 name,它会把 name 作为主键用于关联数据点和 GeoJSON 地理要素。即 data 中的 name 的值只要与 GeoJSON 中properties 中的 name 的值一一对应上,就能正常显示出热力图。

但要是对不上了?china-new-v5.json 中默认是 上海江苏新疆,但如果后端返回给前端的数据是 上海市江苏省新疆维吾尔自治区 呢?

显然,有时候用 name 去作为数据与 GeoJSON 映射的主键,会出现问题。

当然如果前后端约束好了,并且数据来源明确,不会出现乱糟糟的数据,直接用 name 当然没问题。

可以通过修改 series-map.nameProperty 来修改默认的关联主键。但设置什么字段比较合适呢?我们回过头来观察下两个来源的 GeoJSON,会发现有一个东西是唯一的,那就是行政区划代码,ECharts 的叫 id,DataV.GeoAtlas 的叫 adcode

img

一般公司都会爬一份行政区划代码,作为基础数据使用。

目前最新的城乡区划代码可以参考:国家统计局>>统计用区划和城乡划分代码。这是最新的标准,实际使用的时候,一般只需要取前 6 位使用。

img

img

其中省份区划代码没有明确标注,可以通过截取市的区划代码的前两位,后面拼上 6 个 0,也可以直接百度百科查看,省份的区划代码很多年没变了。

其中也不包含我国台湾省、香港特别行政区和澳门特别行政区。台湾省:710000,香港特别行政区:810000,澳门特别行政区:820000

如果你的地图有下钻功能,那么 GeoJSON 中的城乡区划代码可能是旧的,需要根据实际后端返回的数据做更新。

另外,在显示热力图时,china-new-v5.json 的南海诸岛由于属于 GeoJSON 的一个区域,所以也会显示 tooltip

img

可以通过以下设置隐藏掉:

js
data.unshift({
  name: '南海诸岛',
  value: 0,
  itemStyle: {
    opacity: 0,
    label: { show: false }
  },
  tooltip: {
    extraCssText: 'opacity: 0;'
  }
})
data.unshift({
  name: '南海诸岛',
  value: 0,
  itemStyle: {
    opacity: 0,
    label: { show: false }
  },
  tooltip: {
    extraCssText: 'opacity: 0;'
  }
})

以上,希望对你有用。

Last updated: