Hicharts实现世界地图思路及踩过的坑(含中文GeoJSON数据集及详细代码)

2,382 阅读13分钟

目录

一、图表库的比较

二、Geojson世界地图数据集及中文翻译

三、Highcharts实现世界地图(含详细代码)

四、实现过程中踩过的坑

五、Highcharts legend图例禁止点击事件的探索及解决


项目开发过程中,遇到一个需求,在世界地图相应国家中,标注相应参数。最后做出的效果如下图所示,这是符合需求的。接下来我会写一下实现思路以及在实现到发布过程中,遇到的问题。

一、图表库的比较

提到世界地图,我们应该很快想到几个图表库。

(1)Echarts:这个库我们并不陌生,官方给出的介绍是:ECharts,一个使用 JavaScript 实现的开源可视化库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(IE8/9/10/11,Chrome,Firefox,Safari等),底层依赖矢量图形库 ZRender,提供直观,交互丰富,可高度个性化定制的数据可视化图表。

具体的特性及相关介绍可查看官网:echarts.apache.org/zh/feature.…,介绍的很详细。

(2)Highcharts:Highcharts 是一个用纯 JavaScript 编写的一个图表库, 能够很简单便捷的在 Web 网站或是 Web 应用程序添加有交互性的图表。本次来写地图相关的内容,我才知道Highcharts 系列软件包含 Highcharts JS,Highstock JS,Highmaps JS 共三款软件,均为纯 JavaScript 编写的 HTML5 图表库。

非常牛非常好用, 但是它部分功能是要收费的, 使用之前要让公司帮你买好相应的功能才能用于商用。

同样,具体的内容可查看官网:www.highcharts.com.cn/docs/start-…

(3)G2:G2 是一套基于可视化编码的图形语法,以数据驱动,具有高度的易用性和扩展性,用户无需关注各种繁琐的实现细节,一条语句即可构建出各种各样的可交互的统计图表。

具体的内容可查看官网:www.yuque.com/antv/g2-doc…

(4)D3D3.js是一个基于数据操作文档的 JavaScript 库。D3帮助您使用 HTML、SVG 和 CSS 使数据栩栩如生。D3 对 Web 标准的强调为您提供现代浏览器的全部功能,而无需将自己束缚在专有框架中,结合了强大的可视化组件和数据驱动的 DOM 操作方法。

具体的内容可查看官网:d3js.org/

图表库的比较:

www.cnblogs.com/duole/p/110…这篇文章总结了近20个图表库

DataVAntVEchartsHighchartsD3js
特性(1)DataV是一个基于Vue的数据可视化组件库(当然也有React版本)(2)提供用于提升页面视觉效果的SVG边框和装饰(3)提供常用的图表如折线图等(1)AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单方便、专业可靠、无限可能的数据可视化最佳实践。(2)G2本身是一门图形语法,简易性和灵活性是在于你学习了这么语法之后,你能够所思即所得,用图形去表达。而他的难点是在于简易性和灵活性建立在学习了语法之后,G2的定位不是一门开箱即用的框架。ECharts,一个纯 Javascript 的图表库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(IE8/9/10/11,Chrome,Firefox,Safari等),底层依赖轻量级的 Canvas 类库 ZRender,提供直观,生动,可交互,可高度个性化定制的数据可视化图表。让数据可视化更简单 兼容 IE6+、完美支持移动端、图表类型丰富、方便快捷的 HTML5 交互性图表库D3js 是一个可以基于数据来操作文档的 JavaScript 库。可以帮助你使用 HTML, CSS, SVG 以及 Canvas 来展示数据。D3 遵循现有的 Web 标准,可以不需要其他任何框架独立运行在现代浏览器中,它结合强大的可视化组件来驱动 DOM 操作。D3.js 是一个免费的JavaScript库,可以帮助您使用数据创建图像。该工具使您能够将任意数据连接到文档对象模型(DOM),然后将数据驱动的转换应用于文档。通过DOM编程API,程序员可以将文档作为对象访问。
官方地址datav.jiaminghi.com/guide/antv.vision/zhecharts.apache.org/zh/index.ht…www.highcharts.com.cn/docs/how-to…d3js.org/
收费情况阿里免费阿里系免费百度开源免费美国商用的话需要付费免费

二、Geojson世界地图数据集及中文翻译

Highcharts官方提供的数据集地址为:img.hcharts.cn/mapdata/

如何使用数据集及数据集字段解析?www.highcharts.com.cn/docs/mapdat…

上述提到了,Highcharts 系列软件包含 Highcharts JS,Highstock JS,Highmaps JS 共三款软件。在实现地图的时候,要使用Highmaps,Highmaps 是继承自 Highcharts 的专门用于地图的图表插件。Highmaps 除了根据值展示地理区域色块外,还支持线段(可以表示公路,河流等)、点(城市,兴趣点等)等其他地理元素。

使用Highmaps:www.highcharts.com.cn/docs/highma…

引入Highcharts的方案:www.highcharts.com.cn/docs/instal…

(1)Geojson数据是什么?(介绍来源于mp.weixin.qq.com/s/gBkQi1dV3…,这篇文章讲解很详细,可以直接跳转到该文章查看相关内容)

  1. geojson是用json的语法表达和存储地理数据,可以说是json的子集,它不是专门js使用的这点要清楚。
  2. 地图上有山川, 河流, 海洋等等的地理信息, ,那么如何描述一条河? 这个时候就要使用geojson格式的文件来描绘。
  3. 并不是必须用geojson, geojson只是一套规范, 各大解析器用这套规范来解析生成对应的景色, 我们完全可以制定自己的规范来实现这些, 无非是兼容性不好需要自己写绘制的解析器。

(2)Geojson详细介绍?

1. 基本结构

{ // 可以包括点线面, 一个大的集合
  "type": "FeatureCollection", // 定义这个是个geojson文件, 这里还可以是其他值下面会说
  "features": [] // 这里放要绘制的数据
}

以后我们看到"type": "FeatureCollection"这样一行就说明这个文件是geojson规范的文件

2. 描述一个点(Feature)
地图上的打点数据

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",  // 表示这个对象是一个要素
      "properties": {}, // 这里放样式, 后面会专门说
      "geometry": { // 这里面放具体的数据
        "type": "Point",  // 专指画点
        "coordinates": [105.380859375, 31.57853542647338] // 默认是经度与纬度, 三维的话就是xyz三个值, 当然这里也不一定是经纬度(不同的坐标体系)中会讲为什么
      }
    },
  ]
}

3. 描述多个点(FeatureCollection)
**优点

  1. 写法简洁
  2. 这些点样式可以共用
{
  "type""FeatureCollection",
  "features": [
    {
      "type""Feature",
      "properties": {},
      "geometry": {
        "type""MultiPoint"// 多点, 也就是连续画多个同样的点
        "coordinates": [[105.38085937531.57853542647338],
        [105.580859375, 31.52853542647338]
        ]
      }
    },
  ]
}

4. 描述一条线(LineString)

  1. 这里还是描绘每一个点, 但这些点会连接在一起形成线
  2. 地图上的连线数据
{
  "type""FeatureCollection",
  "features": [
    {
      "type""Feature",
      "properties": {},
      "geometry": {
        "type""LineString", // 这里所有的点会连接在一起形成线
        "coordinates"[[105.6005859375, 30.65681556429287],
        [107.95166015624999, 31.98944183792288],
        [109.3798828125, 30.031055426540206],
        [107.7978515625, 29.935895213372444]]
      }
    },
  ]
}

5. 描述多条线(MultiLineString)

  1. 这里第二组与第一组的线, 可以分隔开不会首尾相连.
{
  "type""FeatureCollection",
  "features": [
    {
      "type""Feature",
      "properties": {},
      "geometry": {
        "type""MultiLineString",
        "coordinates":
          [
            [
              [105.6005859375, 30.65681556429287],
              [107.95166015624999, 31.98944183792288],
              [109.3798828125, 30.031055426540206],
              [107.7978515625, 29.935895213372444]
            ],
            [
              [109.3798828125, 30.031055426540206],
              [107.1978515625, 31.235895213372444]
            ]
          ]
      }
    },
  ]
}

6. 描述一个面(Polygon, 也叫多边形)

  1. 第一个点与最后一个点要相同, 这样才能完成闭环!!
  2. 三维数组的格式需要注意
{
  "type""FeatureCollection",
  "features": [
    {
      "type""Feature",
      "properties": {},
      "geometry": {
        "type""Polygon"// 注意这里是三维数组
        "coordinates": [
          [
            [106.10595703125, 33.33970700424026],
            [106.32568359375, 32.41706632846282],
            [108.03955078125, 32.2313896627376],
            [108.25927734375, 33.15594830078649],
            [106.10595703125, 33.33970700424026]
          ]
        ]
      }
    },
  ]
}

7. 一个面里面有多个面(Polygon)

  1. 这种单一的'Polygon'里面出现多个形状, 会出现中空的情况, 类似布尔运算, 这样就可以在地图中描述那种圈型的国家
{
  "type": "FeatureCollection",
  "features": [    {      "type""Feature",      "properties": {},      "geometry": {        "type""Polygon",        "coordinates": [          [            [              -39.7265625,              -3.162455530237848            ],
            [              127.96875,              -3.162455530237848            ],
            [              127.96875,              74.1160468394894            ],
            [              -39.7265625,              74.1160468394894            ],
            [              -39.7265625,              -3.162455530237848            ]
          ],
          [            [              -22.5,              15.961329081596647            ],
            [              110.74218749999999,              15.961329081596647            ],
            [              110.74218749999999,              70.8446726342528            ],
            [              -22.5,              70.8446726342528            ],
            [              -22.5,              15.961329081596647            ]
          ]
        ]
      }
    }
  ]
}

效果如下:
图片

8. 描述多个面(MultiPolygon)
优势:

  1. 写法简洁
  2. 这些点样式可以共用
{
  "type": "FeatureCollection",
  "features": [    {      "type""Feature",      "properties": {},      "geometry": {        "type""MultiPolygon",        "coordinates": [          [          [            [              -39.7265625,              -3.162455530237848            ],
            [              127.96875,              -3.162455530237848            ],
            [              127.96875,              74.1160468394894            ],
            [              -39.7265625,              74.1160468394894            ],
            [              -39.7265625,              -3.162455530237848            ]
          ]
        ],
        [          [            [              -22.5,              15.961329081596647            ],
            [              110.74218749999999,              15.961329081596647            ],
            [              110.74218749999999,              70.8446726342528            ],
            [              -22.5,              70.8446726342528            ],
            [              -22.5,              15.961329081596647            ]
          ]
        ]
        ]
      }
    }
  ]
}

这里如果重叠了就是颜色的叠加了如图所示:
图片

9. 描述一个组(geometries)

  1. 比如我们为了表示一种特定的地貌那么我们可以把这个地貌数据独立起来
{
  "type""FeatureCollection",
  "features": [
    { // 可以包括点线面, 一个独立的集合
      "type""GeometryCollection",
      "geometries": [
        {
          "type""Point",
          "coordinates": [108.6231.02819]
        }, {
          "type""LineString",
          "coordinates"[[108.896484375, 30.1071178870],
          [108.2184375, 30.91717870],
          [109.5184375, 31.2175780]]
        }
      ]
    }
  ]
 }

10. 不同的样式(properties)

{
  "type": "FeatureCollection",
  "features": [
     {
      "type": "Feature",
      "properties": { // 专门放属性
        "stroke": "#fa9661", // 外边颜色
        "stroke-width": 4.1, // 外边宽
        "stroke-opacity": 0.7, // 外边透明度
        "fill": "#9e290c",  // 填充色
        "fill-opacity": 0.7 // 填充色透明度
      },
      "geometry": {
        "type": "Point",  // 画点
        "coordinates": [105.380859375, 31.57853542647338]
      }
    },
  ]
}

三、Highcharts实现世界地图(含详细代码)

ECharts实现地图Demo:blog.csdn.net/wangcunhuaz…

别人的实现Demo(记录使用Highmaps创建世界地图):blog.csdn.net/qq_42282789…

别人的实现Demo(关于在echarts中使用geo.json渲染地图的方法):juejin.cn/post/697123…

在Vue中使用Highcharts:blog.jianshukeji.com/highcharts/…

highcharts-Highmaps 动态传入城市名称:www.bbsmax.com/A/A2dmZGABz…

Highcharts实现世界地图官方DEMO:jshare.com.cn/mapdata/CN4…

Highcharts提供的Geojson世界地图数据集的国家名称中文翻译

其中,我需要展示的国家名称是中文,但是找了好多数据集全是英文,因此对国家名称进行了统一的翻译,主要是根据**提供的语言翻译包来进行对比处理的。有需要该文档的同学可以联系我。

翻译资源如图:

世界地图具体实现代码:

html代码:

<!-- 地图数据信息,用于地图数据地址与名字的映射 -->
<div id="container">加载地图数据中</div>

js代码: 

import template from './template.html'
import './style.less'
// 数据集,翻译之后的,保存到本地了
import { WORLDDATA } from './world'
import Highmaps from 'highcharts/modules/map'
Highmaps(window.Highcharts)

let WorldMapComponent = {
  name: 'worldmap',
  props: {
    worldMapData: {
      type: Array,
      default: function () {
        return {}
      }
    }
  },
  data () {
    return {
      worldData: WORLDDATA
    }
  },
  watch: {
    worldMapData (data) {
      this.initMapData(data)
    }
  },
  methods: {
    initMapData (resData) {
      var mapdata = this.worldData
      var data = []
      resData.length && resData.forEach(element => {
        mapdata.features.length && mapdata.features.forEach(md => {
          if (mapdata.features) {
            if (element.geo.includes(md.properties['name'])) {
              let color = ''
              switch (element.level) {
                case 'a':
                  color = '#67cc02'
                  break
                case 'b':
                  color = '#ffe100'
                  break
                case 'c':
                  color = '#fab011'
                  break
                case 'd':
                  color = '#ff523f'
                  break
              }
              data.push({
                'hc-key': md.properties['hc-key'],
                value: element.counts,
                color: color
              })
            }
          }
        })
      })
      // 初始化图表
      window.$('#container').highcharts('Map', {
        mapNavigation: {
          enabled: true,
          buttonOptions: {
            verticalAlign: 'bottom'
          }
        },
        // 控制图例
        colorAxis: {
          // 下面的图例是长条渐变色
          // min: 0,
          // stops: [
          //   [0, window.Highcharts.getOptions().colors[5]],
          //   [0.5, window.Highcharts.getOptions().colors[4]],
          //   [0.75, window.Highcharts.getOptions().colors[3]],
          //   [1, window.Highcharts.getOptions().colors[13]]
          // ]
          dataClasses: [{
            from: -1,
            to: 0,
            color: '#67cc02',
            name: 'a'
          }, {
            from: 0,
            to: 1,
            color: '#ffe100',
            name: 'b'
          }, {
            from: 2,
            to: 3,
            name: 'c',
            color: '#fab011'
          }, {
            from: 3,
            to: 4,
            name: 'd,
            color: '#ff523f'
          }]
        },
        tooltip: {
          formatter: function () {
            let str = ''
            this.point.colorArr.forEach((item, index) => {
              console.log(item)
              // eslint-disable-next-line
              str += `<span style="color: ${item}">●</span>攻击次数: <b>${this.point.valueArr[index]}</b><br>`
            })
            // eslint-disable-next-line
            return `位置: <b>${this.point.name}</b><br>${str}`
          }
        },
        // 不显示标题
        title: {
          text: null
        },
        // 去掉highcharts的水印
        credits: {
          enabled: false
        },
        series: [{
          data: data,
          mapData: mapdata,
          joinBy: 'hc-key',
          name: 'abcd',
          states: {
            hover: {
              color: '#a4edba'
            }
          },
          dataLabels: {
            enabled: false,
            format: '{point.name}'
          }
        }]
      })
    }
  },
  template
}
WorldMapComponent.install = function (Vue) {
  Vue.component(this.name, WorldMapComponent)
}
export default WorldMapComponent

四、实现过程中踩过的坑

(1)报错:Uncaught Error: Highcharts error #16 

解决方案参考:blog.csdn.net/w926498/art…

(2)报错:Uncaught TypeError: (intermediate value)(intermediate value)(...) is not a funtion:解决方案参考:blog.csdn.net/xubuhui/art…

(3)世界地图实现后,本地运行没有任何问题,但是部署到线上的时候,报错:

Uncaught TypeError: Array.prototype.forEach called on null or undefined at highcharts.js

当时但从报错来看,以为没有处理空数据的情况,因此把所有涉及数组的地方都处理了一遍,重新部署,结果还是报同样的错。 

最后发现是highcharts版本的问题,本地用的是6.2.0,但是部署后线上用的是6.0.7,将线上的版本替换,问题解决。

(4)legend由长条形改为圆形

// 控制图例
        colorAxis: {
          // 下面的图例是长条渐变色
          // min: 0,
          // stops: [
          //   [0, window.Highcharts.getOptions().colors[5]],
          //   [0.5, window.Highcharts.getOptions().colors[4]],
          //   [0.75, window.Highcharts.getOptions().colors[3]],
          //   [1, window.Highcharts.getOptions().colors[13]]
          // ]
          dataClasses: [{
            from: -1,
            to: 0,
            color: '#67cc02',
            name: '低危'
          }, {
            from: 0,
            to: 1,
            color: '#ffe100',
            name: '中危'
          }, {
            from: 2,
            to: 3,
            name: '高危',
            color: '#fab011'
          }, {
            from: 3,
            to: 4,
            name: '危急',
            color: '#ff523f'
          }]
        },

(5)不显示highcharts的水印:

// 去掉highcharts的水印
credits: {
    enabled: false
},

五、Highcharts legend图例禁止点击事件的探索及解决

这个问题我之前没有注意到,测试提了一个bug。确实,这样处理之后,点击下面的图例,只有对应点击的图例会置灰,但是图标上没有任何变化,这肯定是不行的。

这个问题如何解决,我探索了好久。最初的方向是,点击之后,如何才能关联到图表中的数据,做出相应的改变,所以我从官网看了好多demo。

比如这个:www.highcharts.com.cn/demo/highma…,人家的demo命名没有做特殊处理,点击小圆圈就会改变UI的。

还有这个:jshare.com.cn/highmaps/hh…

还有这个:jshare.com.cn/highmaps/I4…

我发现他们都没有做单独特殊的处理,那我在这儿应该也不用做处理才对。但是我的并没有生效。

后来我懂了,我这个图和上述几个demo的性质是不一样的。我这个图是用世界地图改造而来的。

我这个图的原型是这样子:www.highcharts.com.cn/demo/highma…

 图例很明显,跟我现在用的不一样,并且上图中,点击图例之后,世界地图并没有任何改变。而我现在是加了下述代码之后实现的圆点的图例。目前为止,我觉得是这个原因。

        // 控制图例
        colorAxis: {
          dataClasses: [{
            name: '危急',
            color: '#ff523f'
          }, {
            name: '高危',
            color: '#fab011'
          }, {
            color: '#ffe100',
            name: '中危'
          }, {
            color: '#67cc02',
            name: '低危'
          }]
        },

这个时候我改变了方向,能否控制图例,不能触发点击事件。确实,对应这个问题是有相应API的,就是这个legendItemClick,无论是官网还是很多人的介绍,都是用它,但是我加上之后并没有效果。

大家几乎都是这样来处理的,但是我的并没有效果。

       plotOptions: {
          series: {
            events: {
              //控制图标的图例legend不允许切换
             legendItemClick: function (even){                                    
                return false  //return  true 则表示允许切换
              }
            }
          }
        },

其实针对colorAxis,官方也提供了相应的legendItemClick事件,但是同样。我处理的时候并没有生效。官方介绍:api.highcharts.com/highcharts/…

现在这个问题终于解决了,是看到有同学自定义了方法,给了我启发,同学的介绍如链接:blog.csdn.net/weixin_4386…

我用上之后,生效了,阻止了事件的触发。

      // 自定义图例事件
      window.Highcharts.wrap(window.Highcharts.Legend.prototype, 'renderItem', 
        function(proceed, item) {
        proceed.call(this, item)
        var element = item.legendGroup.element
        // 图例点击事件
        element.onclick = function() {
          return false
        }
      })

看了下官网,原来这是提供的二次扩展的一个API。

通过 prototype 重新函数

JavaScript 在动态更改脚本行为方面表现的非常强大。在 Highcharts 内部,我们创建了一个工具函数 wrap,该函数可以在现有原型函数执行之前或之后添加自己的业务代码,其构造及参数说明如下:

Highcharts.wrap(object obj, String methodName, function callback)

  • obj:需要封装的函数的父级对象
  • methodName: 函数名
  • callback: 回调函数

其中原始函数以第一个参数传递给封装函数,原始函数的其他参数在第一个参数后传递给封装函数,下面是示例代码:

H.wrap(H.Series.prototype, 'drawGraph', function(proceed) {

  // 原始函数执行之前编写的逻辑代码
  console.log("We are about to draw the graph: ", this.graph);

  // 执行原始函数(proceed), arguments 为原始函数参数(第一个参数是原始函数)
  proceed.apply(this, Array.prototype.slice.call(arguments, 1));

  // 原始函数执行之后的逻辑代码
  console.log("We just finished drawing the graph: ", this.graph);
});