你手头或许有包含地理坐标的数据,比如产品在不同地区被使用的情况。苦于手头缺乏可用的工具,你或许只能利用这些数据做扇形图或者直方图。现在你知道ECharts支持空间数据的可视化,那么为什么不试着在地图上展示你的数据呢?
现在请跟随笔者完成下面这张图表,它描述的是2017年全国主要城市的工业及居民用电情况(笔者非常热爱自己的国家,但是由于统计方式的不同,数据中不包含香港澳门及台湾的结果)。
数据来源:《中国城市统计年鉴-2018》(收录了2017年全国各级城市社会经济发展等方面的主要统计数据)。
正式开始
-
数据获取:
本次的数据来源已经给出。笔者通过钞能力获得了年鉴的 pdf 版本(412页),其中342-349页为主要城市用水、用电情况。笔者使用 wps 将需要的内容分割出来,并转化为 word 版本,将之粘贴到 excel 表中,调整格式,最终得到了可用的数据(笔者这种脱裤子放屁的操作源于 pdf 转 excel 的结果完全无法使用,格式全崩,但是 word 结果非常完美,粘贴到 excel 表中即可使用)。并且从网络上可以免费获得全国主要城市区县的经纬度数据。
现在,你已经得到了本次绘制图表所需的全部数据。
-
数据分析:
现在我们简要分析一下数据的准确性,并将其转化为 [json] 格式,以供直接使用。
我们注意到部分城市的数据出现了缺失(这很正常),由于其数量不多,故填充为0。现在我们检查一下,是否共计31个省市自治区(答案是“是的”),现在应该没有问题了,我们使用表格工具,将excel表内容转化为 [json] 格式。
两个数据分别是:
数据准备全部完成,接下来我们准备绘制初始图表。
-
初始图表+图表美化:
有了上个散点图的基础,地图的绘制也变得相当简单。散点还是散点,只是坐标系换成了地图,坐标值是经纬度。
-
我们引入ECharts,使用 [jsonp] 方式引入经纬坐标、工业和居民用电的数据。
笔者认为有必要详细介绍一下[jsonp]方法(尽量用通俗语句):
为什么我不能随意引用数据?因为浏览器出于安全考虑,会禁止引用非同源的数据(非同源也就是地址不同,外部的数据难以保证安全性),但是函数并不受限制;
那我应该怎么办?所以我们需要做的就是将数据封装在函数中,读者如果阅读前文提供的 [json] 数据,就会发现数据全都被封装在了一个函数中(比如居民用电数据就被封装在 'cityValueHousehold' 这个函数的括号中);
我接下来该如何引用数据?你首先需要创建一个回调函数,并且函数名称要与 [json] 数据中的函数名相同。然后在后文新建一个<script type="text/javascript" src="文件地址?callback=函数名"></script>引入数据来源并且调用回调函数,这样你回调函数中的 [data] 变量就是你的数据本身了,这时你可以将数据赋给一个全局变量(下方代码中经纬度和工业用电量),或者直接调用它(居民用电量);
如果还有不懂,请联系作者(guo842400579@gmail.com)。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>ECharts</title> <!-- 引入 echarts.js --> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts-gl/dist/echarts-gl.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts-stat/dist/ecStat.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/dist/extension/dataTool.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/map/js/china.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/map/js/world.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/dist/extension/bmap.min.js"></script> <script type="text/javascript" src="https://www.jb51.net/jslib/jquery/jquery.min.js"></script> </head> <body> <!-- 为ECharts准备一个具备大小(宽高)的Dom --> <div id="main" style="width:100%;height:900px;"></div> <script type="text/javascript"> // 基于准备好的dom,初始化echarts实例 var myChart = echarts.init(document.getElementById('main')); //jsonp方式 引入经纬坐标数据的方法,下方使用回调函数为全局变量 geoCoordMap 赋值 var geoCoordMap = []; function geoMap(data){ geoCoordMap = data; }; //引入工业用电量的方法,下方使用回调函数为全局变量 elcforIndustrialValue 赋值 var elcforIndustrialValue = []; function cityValueIndustrial(data){ elcforIndustrialValue = data; console.log(elcforIndustrialValue); }; //引入居民用电量的方法,函数中的 data 即为对应 json 中的数据 function cityValueHousehold(data){ //后续编码在此处进行 //后续编码在此处进行 }; </script> <!--回调函数为全局变量 geoCoordMap 赋值--> <script type="text/javascript" src="https://guozhe-1300242195.cos.ap-beijing.myqcloud.com/2020-01-18-geoCoordMap.json?callback=geoMap"></script> <!--回调函数为全局变量 elcforIndustrialValue 赋值--> <script type="text/javascript" src="https://guozhe-1300242195.cos.ap-beijing.myqcloud.com/2020-01-18-elcforIndustrial.json?callback=cityValueIndustrial"></script> <!--回调函数,当前data值即为居民用电量数据--> <script type="text/javascript" src="https://guozhe-1300242195.cos.ap-beijing.myqcloud.com/2020-01-18-householdElectricity.json?callback=cityValueHousehold"></script> </body> </html>
-
-
我们在回调函数 [cityValueHousehold(data)] 中完成后续的代码,使用 [option] 来表述图表信息。与之前不同的是,我们使用 [option.geo] 绘制地图,将其作为坐标系,在地图上绘制散点。并且由于我们有两份数据,所以我们在地图上绘制两次散点(只需要在 series 中用逗号隔开,并用中括号包裹即可 [{},{}] ):
var option = { //地图背景 backgroundColor: 'rgba(1, 1, 1, 1)', //图表标题,已经设置了部分的规则 title: { text: '2017年全国主要城市用电情况',//标题内容 x:'center',//居中 top : 30 ,//距离上端30像素 //标题字体设置 textStyle: { color: '#fff',//字体颜色 fontSize:30,//字体大小 } }, //提示框组件 tooltip: {}, //图例组件 //legend: {}, //视觉映射组件 //visualMap: {}, //geo组建绘制地图,下方详细介绍各设置 geo: { //地图:中国 map: 'china', //是否允许缩放,拖动 roam: false, //缩放设置 scaleLimit:{ min:1//缩放最小程度为1,即最初状态 }, //文字设置(指各省名称) label: { //仅当选中状态下有设置 emphasis: { color: '#fff', //字体颜色白色 show: true, //是否显示文字,当前‘是’ } }, //距离顶端75个像素 top: 75, //各板块设置 itemStyle: { //通常状态下的设置 normal: { areaColor: '#2a333d', //板块颜色 borderColor: '#111' //板块边框颜色 }, //选中状态下的设置 emphasis: { areaColor: 'rgba(10,10,10,1)', //板块颜色 borderColor: '#fff', //边框颜色 shadowBlur: 3, //阴影宽度,3像素 shadowColor: '#ffff99', //阴影颜色 } } }, series: [ { //浮点图表示城市工业用电情况 //系列名称,用于tooltip的显示,legend 的图例筛选 name: '工业', //类型:散点图 type: 'scatter', //系列使用的坐标系,可选'cartesian2d'笛卡尔坐标系,'polar'极坐标系,'geo'当前的地理坐标系 coordinateSystem: 'geo', //引入数据,当前为空 data: [], //字体设置 label: { //通常状态 normal: { show: false //不显示文字 }, //选中状态 emphasis: { show: false //不显示文字 } }, //图形设置 itemStyle: { //通常状态 normal: { color:'#eac736', //圆形的颜色 shadowBlur: 20, //阴影宽度 shadowColor: '#ffff99',//阴影颜色 borderColor: '#fff', //边框颜色 borderWidth: 1 //边框宽度 }, //选中状态 emphasis: { borderColor: '#fff', //边框颜色 borderWidth: 2 //边框宽度 } }, }, { //浮点图表示城市居民用电情况 //内容完全相同,不再做额外注释 name: '居民', type: 'scatter', coordinateSystem: 'geo', //引入数据,当前为空 data: [], label: { normal: { show: false }, emphasis: { show: false } }, itemStyle: { normal: { color:'#50a3ba', shadowBlur: 20, shadowColor: '#ffff99', borderColor: '#fff', borderWidth: 1 }, emphasis: { borderColor: '#fff', borderWidth: 2 } }, } ] }
保存代码,现在可以看到地图已绘制好,选中区域显示省(自治区)名称(简称):
-
接下来我们引入数据,在地图上绘制我们的散点图。这里需要引入一个函数 [convertData()],这个函数可以为我们数据中的城市和用电量之间增加经纬度值,即使数据变为:
这样的数据即可用于我们的图表(为什么不直接令最初的数据就是这个结构?)。
//引入居民用电量的方法,函数中的 data 即为对应 json 中的数据 function cityValueHousehold(data){ //函数converData(),可在地区的经纬度后添加value值 var convertData = function (data) { var res = []; for (var i = 0; i < data.length; i++) { var geoCoord = geoCoordMap[data[i].name]; //console.log(geoCoord); if (geoCoord) { res.push({ name: data[i].name, value: geoCoord.concat(data[i].value) }); } } return res; }; //console.log(convertData(data)); var option = { //地图背景 backgroundColor: 'rgba(1, 1, 1, 1)', //图表标题,已经设置了部分的规则 title: { text: '2017年全国主要城市用电情况',//标题内容 x:'center',//居中 top : 30 ,//距离上端30像素 //标题字体设置 textStyle: { color: '#fff',//字体颜色 fontSize:30,//字体大小 } }, //提示框组件 tooltip: {}, //geo组建绘制地图,下方详细介绍各设置 geo: { //地图:中国 map: 'china', //是否允许缩放,拖动 roam: false, //缩放设置 scaleLimit:{ min:1//缩放最小程度为1,即最初状态 }, //文字设置(指各省名称) label: { //仅当选中状态下有设置 emphasis: { color: '#fff', //字体颜色白色 show: true, //是否显示文字,当前‘是’ } }, //距离顶端75个像素 top: 75, //各板块设置 itemStyle: { //通常状态下的设置 normal: { areaColor: '#2a333d', //板块颜色 borderColor: '#111' //板块边框颜色 }, //选中状态下的设置 emphasis: { areaColor: 'rgba(10,10,10,1)', //板块颜色 borderColor: '#fff', //边框颜色 shadowBlur: 3, //阴影宽度,3像素 shadowColor: '#ffff99', //阴影颜色 } } }, series: [ { //浮点图表示城市工业用电情况 //系列名称,用于tooltip的显示,legend 的图例筛选 name: '工业', //类型:散点图 type: 'scatter', //系列使用的坐标系,可选'cartesian2d'笛卡尔坐标系,'polar'极坐标系,'geo'当前的地理坐标系 coordinateSystem: 'geo', //引入数据,使用函数处理原数据 elcforIndustrialValue ,为其增加经纬值 data: convertData(elcforIndustrialValue), //字体设置 label: { //通常状态 normal: { show: false //不显示文字 }, //选中状态 emphasis: { show: false //不显示文字 } }, //图形设置 itemStyle: { //通常状态 normal: { color:'#eac736', //圆形的颜色 shadowBlur: 20, //阴影宽度 shadowColor: '#ffff99',//阴影颜色 borderColor: '#fff', //边框颜色 borderWidth: 1 //边框宽度 }, //选中状态 emphasis: { borderColor: '#fff', //边框颜色 borderWidth: 2 //边框宽度 } }, }, { //浮点图表示城市居民用电情况 //内容完全相同,不再做额外注释 name: '居民', type: 'scatter', coordinateSystem: 'geo', //引入数据,使用函数处理原数据 data ,为其增加经纬值 data: convertData(data), label: { normal: { show: false }, emphasis: { show: false } }, itemStyle: { normal: { color:'#50a3ba', shadowBlur: 20, shadowColor: '#ffff99', borderColor: '#fff', borderWidth: 1 }, emphasis: { borderColor: '#fff', borderWidth: 2 } }, } ] } // 使用刚指定的配置项和数据显示图表。 myChart.setOption(option); };
不要着急,现在地图中虽然有了散点,但是大小完全相同,无法体现其 [value] (用电量)上的差异,我们使用 [symbolSize] 规则对其进行设置。并且由于数值过大,我们将结果除以100(先开方,但开方并不是由于数值过大):
data: convertData(elcforIndustrialValue), //图标大小设置,使用回调函数,以数值的开方结果/100作为圆形的半径 symbolSize: function (val) { return Math.sqrt( val[2] )/100;//工业用电较多,除以100 },
将这段代码补充到 [data] 之下,现在你已经绘制出了你想要的图表。蓝色点代表居民用电,黄色点代表工业用电。
-
不要着急,我们还需要补充一些东西。
-
[legend] 图例组件。有了图例,只需要点击开启或关闭,整个系列的散点也会相应开启或关闭,便于查看图表结果(交互式图表的基本要求):
//图例组件 legend: [{ //图例列表的布局朝向,'horizontal'横排,'vertical'纵排 orient: 'vertical', //竖直方向位置,当前置底 y: 'bottom', //水平方向位置,当前居右 x:'right', //对应名称 name 为‘工业’的系列 data:['工业'], //文字设置 textStyle: { color: '#fff'//字体白色 } },{ orient: 'vertical', y: 'bottom', //水平方向位置,当前居左 x:'left', //对应名称 name 为‘居民’的系列 data:['居民'], textStyle: { color: '#fff' } }],
-
[visualMap] 视觉映射组件。图例可以将数据映射到视觉元素上(元素大小、颜色等),且优先级高于系列(series)中的元素设置。本例中笔者设置了元素的颜色根据其数值大小渐变,但是由于不便于比较工业和居民用电情况,最终将其注释掉,使之不生效。读者可以尝试将其打开,查看效果:
visualMap: { //组件定义域,最大最小值设置,根据数据所处位置赋予其渐变效果 min: 0, max: 10000000, //是否显示拖拽用的手柄 calculable: true, //颜色范围,三段 inRange: { color: ['#50a3ba', '#eac736', '#d94e5d'] }, //文字规则,颜色白色 textStyle: { color: '#fff' } },
笔者该例中未使用,但是 visualMap 在只有一套数据的图表中相当好用。
-
[tooltip]提示框组件。十分重要的组件,通过设置回调函数,当鼠标落于图形上时,可以显示当前图形的部分信息:
//提示框组件 tooltip: { //触发类型,'item'图形触发,鼠标落于图形上显示;'axis'坐标轴触发;'none'什么都不触发 trigger: 'item', //回调函数,显示当前图形对应的名称及其第三个 value 值(用电量) formatter: function (params) { return params.name + ' : ' + params.value[2]; } },
-
现在给出最终的完整代码,读者可以根据官方文档自行修改,或者使用自己的数据,生成自己的专属图表:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>ECharts</title> <!-- 引入 echarts.js --> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts-gl/dist/echarts-gl.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts-stat/dist/ecStat.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/dist/extension/dataTool.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/map/js/china.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/map/js/world.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/dist/extension/bmap.min.js"></script> <script type="text/javascript" src="https://www.jb51.net/jslib/jquery/jquery.min.js"></script> </head> <body> <!-- 为ECharts准备一个具备大小(宽高)的Dom --> <div id="main" style="width:100%;height:900px;"></div> <script type="text/javascript"> // 基于准备好的dom,初始化echarts实例 var myChart = echarts.init(document.getElementById('main')); //引入经纬坐标数据的方法,下方使用回调函数为全局变量 geoCoordMap 赋值 var geoCoordMap = []; function geoMap(data){ geoCoordMap = data; }; //引入工业用电量的方法,下方使用回调函数为全局变量 elcforIndustrialValue 赋值 var elcforIndustrialValue = []; function cityValueIndustrial(data){ elcforIndustrialValue = data; console.log(elcforIndustrialValue); }; //引入居民用电量的方法,函数中的 data 即为对应 json 中的数据 function cityValueHousehold(data){ //函数converData(),可在地区的经纬度后添加value值 var convertData = function (data) { var res = []; for (var i = 0; i < data.length; i++) { var geoCoord = geoCoordMap[data[i].name]; //console.log(geoCoord); if (geoCoord) { res.push({ name: data[i].name, value: geoCoord.concat(data[i].value) }); } } return res; }; //console.log(convertData(data)); var option = { //地图背景 backgroundColor: 'rgba(1, 1, 1, 1)', //图表标题,已经设置了部分的规则 title: { text: '2017年全国主要城市用电情况',//标题内容 x:'center',//居中 top : 30 ,//距离上端30像素 //标题字体设置 textStyle: { color: '#fff',//字体颜色 fontSize:30,//字体大小 } }, //提示框组件 tooltip: { //触发类型,'item'图形触发,鼠标落于图形上显示;'axis'坐标轴触发;'none'什么都不触发 trigger: 'item', //回调函数,显示当前图形对应的名称及其第三个 value 值(用电量) formatter: function (params) { return params.name + ' : ' + params.value[2]; } }, //图例组件 legend: [{ //图例列表的布局朝向,'horizontal'横排,'vertical'纵排 orient: 'vertical', //竖直方向位置,当前置底 y: 'bottom', //水平方向位置,当前居右 x:'right', //对应名称 name 为‘工业’的系列 data:['工业'], textStyle: { color: '#fff' } },{ orient: 'vertical', y: 'bottom', x:'left', data:['居民'], textStyle: { color: '#fff' } }], // //视觉映射组件 // visualMap: { // //组件定义域,最大最小值设置,根据数据所处位置赋予其渐变效果 // min: 0, // max: 10000000, // //是否显示拖拽用的手柄 // calculable: true, // //颜色范围,三段 // inRange: { // color: ['#50a3ba', '#eac736', '#d94e5d'] // }, // textStyle: { // color: '#fff' // } // }, //geo组建绘制地图,下方详细介绍各设置 geo: { //地图:中国 map: 'china', //是否允许缩放,拖动 roam: false, //缩放设置 scaleLimit:{ min:1//缩放最小程度为1,即最初状态 }, //文字设置(指各省名称) label: { //仅当选中状态下有设置 emphasis: { color: '#fff', //字体颜色白色 show: true, //是否显示文字,当前‘是’ } }, //距离顶端75个像素 top: 75, //各板块设置 itemStyle: { //通常状态下的设置 normal: { areaColor: '#2a333d', //板块颜色 borderColor: '#111' //板块边框颜色 }, //选中状态下的设置 emphasis: { areaColor: 'rgba(10,10,10,1)', //板块颜色 borderColor: '#fff', //边框颜色 shadowBlur: 3, //阴影宽度,3像素 shadowColor: '#ffff99', //阴影颜色 } } }, series: [ { //浮点图表示城市工业用电情况 //系列名称,用于tooltip的显示,legend 的图例筛选 name: '工业', //类型:散点图 type: 'scatter', //系列使用的坐标系,可选'cartesian2d'笛卡尔坐标系,'polar'极坐标系,'geo'当前的地理坐标系 coordinateSystem: 'geo', //引入数据 data: convertData(elcforIndustrialValue), //图标大小设置,使用回调函数,以数值的开方结果/100作为圆形的半径 symbolSize: function (val) { return Math.sqrt( val[2] )/100;//工业用电较多,除以100 }, //字体设置 label: { //通常状态 normal: { show: false //不显示文字 }, //选中状态 emphasis: { show: false //不显示文字 } }, //图形设置 itemStyle: { //通常状态 normal: { color:'#eac736', //圆形的颜色 shadowBlur: 20, //阴影宽度 shadowColor: '#ffff99',//阴影颜色 borderColor: '#fff', //边框颜色 borderWidth: 1 //边框宽度 }, //选中状态 emphasis: { borderColor: '#fff', //边框颜色 borderWidth: 2 //边框宽度 } }, }, { //浮点图表示城市居民用电情况 //内容完全相同,不再做额外注释 name: '居民', type: 'scatter', coordinateSystem: 'geo', //引入数据 data: convertData(data), //图标大小设置,使用回调函数,以数值的开方结果/100作为圆形的半径 symbolSize: function (val) { return Math.sqrt( val[2] )/100;//居民用电较多,除以100 }, label: { normal: { show: false }, emphasis: { show: false } }, itemStyle: { normal: { color:'#50a3ba', shadowBlur: 20, shadowColor: '#ffff99', borderColor: '#fff', borderWidth: 1 }, emphasis: { borderColor: '#fff', borderWidth: 2 } }, } ] } // 使用刚指定的配置项和数据显示图表。 myChart.setOption(option); }; </script> <!--回调函数为全局变量 geoCoordMap 赋值--> <script type="text/javascript" src="https://guozhe-1300242195.cos.ap-beijing.myqcloud.com/2020-01-18-geoCoordMap.json?callback=geoMap"></script> <script type="text/javascript" src="https://guozhe-1300242195.cos.ap-beijing.myqcloud.com/2020-01-18-elcforIndustrial.json?callback=cityValueIndustrial"></script> <script type="text/javascript" src="https://guozhe-1300242195.cos.ap-beijing.myqcloud.com/2020-01-18-householdElectricity.json?callback=cityValueHousehold"></script> </body> </html>
最终的图表如下。左右下角新增了两个图例,点击可开启或关闭:
-
-
图表的意义-它述说了怎样的故事:
对于绝大多数的城市,居民的用电量都小于工业用电量,三亚除外;
工业用电量沿海城市较多。其中珠三角、长三角的居民用电量较多,而山东滨州、河北唐山的居民用电量相当少,说明后者是纯工业城市,前者第三产业比较发达;
对于内陆城市,重庆、包头、鄂尔多斯、乌兰察布、通辽相对较多,显然内蒙古的工业城市要远多于其他地区;
-
地球处于浩瀚星空:
ECharts支持绘制三维图形。笔者将地图(坐标系)换成了三维地球,散点图换做了三维柱形图(bar3D),作出了如下的可视化图形。
或许你的老板比较喜欢这种图形,但笔者认为这个图形并不便于反映数据(虽然它可能真的COOL!)。
如果大家有兴趣,可以访问这个页面自行观察。如果你想绘制三维图形,可以访问ECharts官方GL实例,对照文档,自行学习三维可视化(因为笔者本人也知之甚少)。
故事结束
第二个故事主要讲解如何将空间数据可视化。
地图是一个非常直观的可视化类型。
不论怎样,地图都是一种理解数据的极佳手段。它们是真实世界按比例缩小后的版本,而且它们无处不在。
现在你应该知道如何将手头的空间数据可视化,本例将数据以散点大小(也就是面积)加以展示,除此之外,你还可以将数据以区域颜色反映出来。总而言之,地图具有极强的直观性,可以为你(的努力)带来丰厚的回报:
一方面丰富了我们呈现数据的形式,另一方面它也比静态图表更加利于我们深入地探索数据。
并且,你的空间数据往往还附带时间信息。笔者曾完成了一个动画,展示了自己负责的产品,在半年中的每一天被各个地区调用的情况。这个动画在年终总结会议中可以说赚足了眼球。后文中将会介绍这个方法,但是因为需要额外的 javaScript 编码,所以学习的难度可能相对较大,笔者也会尽量描述清晰。
故事的最后,笔者想强调一点:散点图通过各点「面积」体现相应数值的大小,而非直径(半径)。所以在设置散点半径时务必使用数值的「开平方」结果。如果你直接将数值作为散点的半径,数值上的差别将被放大,你的读者将可能对图表产生误解,进而无法了解实际情况。在绘制散点图时请牢记这一点。
好了,放松下你的头脑,接下来才是重头戏。