3

导语

大数据呈现应用越来越广泛,支持大数据呈现的SDK,水平较高的有echarts、highchart、D3;然而在地图呈现的功能上,大都只能绘制矢量地图,而不能呈现具有真实效果的地图;鉴于此,本文重点在于如何制作一张,即可以看到真实效果,又能进行交互的矢量地图;

先睹为快

可缩放、可交互

本文制作的地图

展示人口流动数据

echarts官方地图示例

若有所思

技术选择

想实现上述效果,最先想到的SDK是TWaver,思路也非常的简单;

  1. 使用Node呈现一张地图背景图片,像素越大越好,缩放效果好;

  2. 使用ShapeNode加载地图数据,并设置好位置、缩放比例等因素,恰好与地图重叠;
    3.控制地图的Layer为底层,不可选中;ShapeNode为上层,可交互;

TWaver绘制地图

iChart & ZRender

本文使用ichart + zrender技术,绘制上述的效果;

为什么使用zrender呢?实际上zrender的功能比较简单,用于绘制基本的形状;其实细心的你会发现,echarts的底层就是使用了zrender;

有为什么使用ichart呢?ichart用于绘制常用的图表,底层基于Canvas绘制,也比较容易改造;而echarts使用的是SVG,修改起来就没那么容易啦!

实验天地

目标一:实现柱单节状图效果

实现柱状图效果,还真没那么容易!ichart不支持怎么办? 定制!
找到ichart的柱状图类:Cylinder.js,好就从改造他开始了!

ichart柱状图

找到绘制网元的方法buildPath

Cylinder.prototype = {
            type: 'cylinder',
            /**
             * 创建圆形路径
             * @param {CanvasRenderingContext2D} ctx
             * @param {module:zrender/shape/Cylinder~ICircleStyle} style
             */
             buildPath : function (ctx, style) {
                 //拿到你的画笔,我就随便画啦
            }
}

如上所言,获取了Canvas的画笔,自由的绘制一个矩形,上下各一个椭圆,不就完事了?

this.ellipse(ctx, style.x, style.y, style.a, style.b);
ctx.fillRect(style.x - style.a, style.y - a, style.a * 2 , d);
this.ellipse(ctx, style.x, style.y - a, style.a, style.b);

封装完毕,打包,混淆,加上测试代码,看效果;

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Cylinder</title>
    <script type="text/javascript" src="../doc/asset/js/esl/esl.js"></script>
</head>
<body>
    <script type="text/javascript">
     var fileLocation = '../build/zrender';
     require.config({
        paths:{ 
            zrender: fileLocation,
            'zrender/shape/Circle': fileLocation,
            'zrender/shape/Cylinder': fileLocation,
        }
    });
     require(["zrender", 'zrender/shape/Circle','zrender/shape/Cylinder'], function(zrender,CircleShape,CylinderShape){
        var zr = zrender.init( document.getElementById("Main") );
        var shape = new CylinderShape({
            style: {
                x: 300,
                y: 300,
                a: 10,
                b: 5,
                height:200,
                brushType: 'both',
                color: 'orange',
                strokeColor: 'red',
                lineWidth: 1,
                text: 'Cylinder'
            },
            highlightStyle:{
                color: 'orange',
                strokeColor: 'red',
                lineWidth: 2,
                text: 'Cylinder'
            },
            draggable : true,
            hoverable:true,
            clickable:true,
        });
        zr.addShape(shape);
    })
</script>
<div id="Main" style="width:1000px;height:600px;"></div>
</body>
</html>

单节柱状图

目标二:实现柱多节柱状图效果

问题来了,如果想实现多段的柱状图,如何是好呢?我想您自己都已经有思路了;多画几段不就完事了吗?

buildPath : function (ctx, style) {
                var rect = this.getRect(style);
                // ctx.strokeRect(rect.x,rect.y,rect.width,rect.height);
                // Better stroking in ShapeBundle
                // ctx.moveTo(style.x + style.a, style.y - style.height/2);
                // ctx.arc(style.x, style.y, style.r, 0, Math.PI * 2, true);
                // ctx.arc(style.x, style.y, style.a, 0, Math.PI * 2, true);
                // this.endDraw(style,ctx);
                var data = style.data, color = style.color, isPercent = style.isPercent || false, maxHeight = style.maxHeight || 100;
                if(isPercent) {
                    if(data instanceof Array) {
                        var data2 = [];
                        var all = 0;
                        for(var i = 0;i<data.length; i++) {
                            all += data[i];
                        }
                        for(var i = 0;i<data.length; i++) {
                            data2.push(maxHeight * data[i]/all);
                        }
                        data = data2;
                    }
                }
                if(data instanceof Array){
                    ctx.fillStyle = 'black';
                    ctx.shadowBlur=15;
                    ctx.shadowColor="black";
                    ctx.strokeStyle = 'rgba(0,0,0,0.1)';
                    ctx.lineWidth = 1;
                    this.ellipse(ctx, style.x, style.y+1, style.a, style.b);
                    ctx.fill();
                    ctx.shadowBlur=0;
                    ctx.lineWidth = 1;
                    this.ellipse(ctx, style.x, style.y, style.a, style.b);
                    var a = 0;
                    for(var i = 0;i < data.length;i++){
                        var d = data[i];
                        if(color instanceof Array){
                            ctx.fillStyle = color[i];
                            ctx.strokeStyle = color[i];
                        }
                        this.endDraw(style,ctx);
                        a += d;
                        ctx.fillRect(style.x - style.a, style.y - a, style.a * 2 , d);
                        this.ellipse(ctx, style.x, style.y - a, style.a, style.b);
                        if(color instanceof Array){
                            ctx.fillStyle = color[i];
                            ctx.strokeStyle = color[i];
                        }
                        this.endDraw(style,ctx);
                    }
                }else{
                    this.ellipse(ctx, style.x, style.y + style.height/2, style.a, style.b);
                    this.endDraw(style,ctx);
                    ctx.fillRect(style.x - style.a, style.y - style.height/2, style.a * 2 , style.height);
                    // ctx.strokeRect(style.x - style.a, style.y - style.height/2, style.a * 2 , style.height);
                    this.ellipse(ctx, style.x, style.y - style.height/2, style.a, style.b);
                    this.endDraw(style,ctx);
                    ctx.moveTo(style.x - style.a, style.y - style.height/2);
                    ctx.lineTo(style.x - style.a,style.y + style.height/2);
                    ctx.fill();
                    ctx.moveTo(style.x + style.a, style.y - style.height/2);
                    ctx.lineTo(style.x + style.a,style.y + style.height/2);
                    ctx.fill();
                }
                // ctx.strokeRect(style.x - style.a, style.y - style.height/2, style.lineWidth , style.height);
                // ctx.strokeRect(style.x + style.a, style.y - style.height/2, style.lineWidth , style.height);
                // this.ellipse(ctx, style.x, style.y+100, style.r, style.r/3);
                return;
            }

多段柱状图

目标三:绘制地图

使用zrender的PolygonShape绘制矢量地图;但是前提是,和底图图片完全吻合的数据哪里来呢?

聪明的我想到了使用TWaver自带编辑器,完美扣除地图数据;得到结果,形如如下数据格式:

< px="1209.5549397107488" y="1242.081312831646"/>
< px="1209.5549397107488" y="1233.5993604641965"/>
< px="1179.8681064246748" y="1212.3944795455723"/>
< px="1184.1090826083996" y="1199.6715509943976"/>
< px="1171.3861540572252" y="1161.502765340874"/>
< px="1162.9042016897754" y="1157.2617891571492"/>

稍微加工处理下,得到如下数据:

{"type": "Feature","properties":{"id":"65","size":"550","name":"新疆","cp":[471.08525328117855,-97.6746544555845],"childNum":18},"geometry":{"type":"Polygon","coordinates":[[[1143.6222085570992,-80.96566177792188],
            [1131.0904640488523,-76.78841360850622],
            [1131.0904640488523,-93.49740628616884],
            [1126.9132158794366,-135.26988798032542],

开始加入数据,创建矢量地图;

var smoothLine = new PolylineShape({
            style : {
              pointList : points,
              smooth : 'spline',
              brushType : 'stroke',
              color : 'white',
              strokeColor : "white",
              lineWidth : 2,
              lineType : 'dotted'
            },
            zlevel:1,
            draggable : true,
          });
          zr.addShape(smoothLine);

Paste_Image.png

最后附上完整代码:

<!DOCTYPE html>
<html>
<head>
  <title></title>
  <meta charset="utf-8">
  <style type="text/css">
    #bg{  
      z-index:1;  
      width:1300px;  
      height:700px;  
      position:absolute;  
      background-color: black,
    }  
    #chart{  
      z-index:2;  
      width:280px;  
      height:150px;  
      position:absolute;  
      -moz-border-radius: 15px; 
      -webkit-border-radius: 15px; 
      border-radius:15px;       
    }  
  </style>
</head>
<body>
  <div id="bg" ></div>
  <div id="chart" ></div>
  <script type="text/javascript" src="esl.js"></script>
  <script type="text/javascript" src="jquery.js"></script>
  <script type="text/javascript" src="echarts-all.js"></script>
  <script>
    var fileLocation = 'zrender';
    require.config({
      paths:{ 
        zrender: fileLocation,
        'zrender/shape/Image': fileLocation,
        'zrender/shape/Polygon': fileLocation,
        'zrender/shape/Polyline': fileLocation,
        'zrender/shape/Circle': fileLocation,
        'zrender/shape/Cylinder': fileLocation,
        'zrender/shape/Text': fileLocation,
      }
    });
    var myChart = echarts.init(document.getElementById('chart'));
    $.getJSON('china.json', function(json, textStatus) {
      require(["zrender", 'zrender/shape/Image','zrender/shape/Polygon', "zrender/shape/Polyline",'zrender/shape/Circle','zrender/shape/Cylinder','zrender/shape/Text'], function(zrender, ImageShape,PolygonShape,PolylineShape,CircleShape, CylinderShape,TextShape){
        zr = zrender.init( document.getElementById("bg"));
        var config = require('zrender/config');
        zr.on(config.EVENT.CLICK,
          function(params) {
            if (!params.target) {
              $('#chart').css('z-index',-1);
              myChart.clear();
            }
          }
          );
        zr.modLayer(0,{
         zoomable:true,
         panable:true,
         clearColor:'#cdcdcd',
         position:[160,50],
         rotation:[0,0],
         scale:[0.25,0.25],
       });
        zr.modLayer(1,{
         zoomable:true,
         panable:true,
         clearColor:'rgba(0,0,0,0)',
         position:[205.5,240.5],
         rotation:[0,0],
         scale:[0.25,0.25],
       });
        var image = new ImageShape({
         position : [0, 0],
         scale : [1, 1],
         style : {
           x : 0,
           y : 0,
           image : "bg_china3.png",
         },
         draggable : false,
         clickable: false,
         hoverable:false,
         zlevel:0,
       });
        zr.addShape( image );
        json.features.forEach(function (feature) {
          var points = [];
          if (feature.geometry.type === 'MultiPolygon') {
            feature.geometry.coordinates.forEach(function (polygon) {
              polygon.forEach(function (coordinate) {
                coordinate.forEach(function (point, i) {
                  points.push(convertPoint(point));
                });
              });
            });
          } else if (feature.geometry.type === 'Polygon') {
            feature.geometry.coordinates.forEach(function (coordinate) {
              coordinate.forEach(function (point, i) {
                points.push(convertPoint(point));
              });
            });
          } else {
            console.log(feature.geometry.type);
          }
          var smoothLine = new PolylineShape({
            style : {
              pointList : points,
              smooth : 'spline',
              brushType : 'stroke',
              color : 'white',
              strokeColor : "white",
              lineWidth : 2,
              lineType : 'dotted'
            },
            zlevel:1,
            draggable : true,
          });
          zr.addShape(smoothLine);

          zr.addShape(new PolygonShape({
            style : {
              pointList : points,
              lineCape:'butt',
              // text:feature.properties.name,
              // textPosition:'inside',
              // textPosition:'inside',//'inside','top','bottom','left','right': 
              // textColor:'black',
              // textAlign:'start',//
              // textBaseline:'hanging',//'hanging'
              // textFont:'bold 32px verdana',
              // smooth : 0.5,
              // smoothConstraint: [[-Infinity, -Infinity], [200, Infinity]],
              brushType : 'both',
              color : (feature.properties.name === '澳门' || feature.properties.name === '香港') ? '#578096' : 'rgba(220, 20, 60, 0)',
              strokeColor : "white",
              lineWidth : 1,
            },
            highlightStyle:{
              // strokeColor:'white',
            },
            draggable : true,
            zlevel:1,
          }));
          var cp = feature.properties.cp;
          zr.addShape(new TextShape({
           style: {
            text: feature.properties.name,
            x: cp[0],
            y: cp[1] + 30,
            textFont: 'bold 32px verdana',
            textColor:'black',
          },
          draggable : false,
          zlevel:1,
        }));

          // var color = ['#C1232B','#C46924','#FCCE10'];
          var color = ['#be1e20','#ff4e00','#ff8400','#ffce00','#c0b900','#94d600','#63ccca','#00a8e6','#005db9','#ac3c73','#853376'];
          var data = [Math.random() * 100,Math.random() * 100,Math.random() * 100];
          var shape = new CylinderShape({
            style: {
              x: cp[0],
              y: cp[1],
              a: 20,
              b: 10,
              brushType: 'both',
              color: color,
              data:data,
              strokeColor: color,
              lineWidth: 1,
              text: "流入" || feature.properties.name,
              textFont:'bold 32px verdana',
            },
            highlightStyle:{
              color: color,
              strokeColor: color,
              lineWidth: 2,
              text: '流入' || feature.properties.name,
              textFont:'bold 32px verdana',
            },
            hoverable:false,
            clickable:true,
            draggable: false,
            zlevel:1,
            onmousedown: function(e){
              if(e.event.detail == 2){
                option = {
                  backgroundColor:'rgba(31,34,37,0.8)',
                  title : {
                    text:feature.properties.name +'(流入)',
                    x:'left',
                    textStyle:{
                      color:'white',
                    }
                  },
                  tooltip : {
                    trigger: 'item',
                    formatter: "{a} <br/>{b} : {c} ({d}%)"
                  },
                  color:['#be1e20','#ff4e00','#ff8400','#ffce00','#c0b900','#94d600','#63ccca','#00a8e6','#005db9','#ac3c73','#853376'],
                  series : [
                  {
                    name:'北京人口',
                    type:'pie',
                    radius : '40%',
                    center: ['50%', '60%'],
                    data:[
                    {value:100, name:'第一类人' + '(' + (100/2230 * 100).toFixed(0)+'%)'},
                    {value:300, name:'第二类人' + '(' + (200/2230 * 100).toFixed(0)+'%)'},
                    {value:400, name:'第三类人' + '(' + (400/2230 * 100).toFixed(0)+'%)'},
                    {value:400, name:'第四类人' + '(' + (400/2230 * 100).toFixed(0)+'%)'},
                    {value:300, name:'第五类人' + '(' + (300/2230 * 100).toFixed(0)+'%)'},
                    {value:250, name:'第六类人' + '(' + (250/2230 * 100).toFixed(0)+'%)'},
                    {value:200, name:'第七类人' + '(' + (200/2230 * 100).toFixed(0)+'%)'},
                    {value:180, name:'第八类人' + '(' + (180/2230 * 100).toFixed(0)+'%)'},
                    {value:100, name:'第九类人' + '(' + (100/2230 * 100).toFixed(0)+'%)'}
                    ]
                  }
                  ]
                };
                var chartDiv = document.getElementById('chart');
                chart.style.left = e.event.clientX + 30 + "px";
                chart.style.top = e.event.clientY - 210/2 + "px";
                myChart.setOption(option);
                $('#chart').css('z-index',2);
              }
            }
          });
          zr.addShape(shape);
          var color = ['#B5C334','#F4E001','#F0805A'];
          var data = [Math.random() * 150,Math.random() * 150,Math.random() * 150];
          var shape = new CylinderShape({
            style: {
              x: cp[0] + 50,
              y: cp[1],
              a: 20,
              b: 10,
              data:data,
              brushType: 'both',
              color: color,
              strokeColor: color,
              lineWidth: 1,
              text: '流出'||feature.properties.name,
              textFont:'bold 32px verdana',
            },
            highlightStyle:{
              color: color,
              strokeColor: color,
              lineWidth: 2,
              text: '流出'||feature.properties.name,
              textFont:'bold 32px verdana',
            },
            hoverable:true,
            clickable:true,
            draggable: false,
            zlevel:1,
            onmousedown: function(e){
              if(e.event.detail == 2){
                option = {
                  backgroundColor:'rgba(31,34,37,0.8)',
                  title : {
                    text:feature.properties.name + '(流出)',
                    x:'left',
                    textStyle:{
                      color:'white',
                    }
                  },
                  tooltip : {
                    trigger: 'item',
                    formatter: "{a} <br/>{b} : {c} ({d}%)"
                  },
                  color:['#be1e20','#ff4e00','#ff8400','#ffce00','#c0b900','#94d600','#63ccca','#00a8e6','#005db9','#ac3c73','#853376'],
                  series : [
                  {
                    type:'pie',
                    radius : '40%',
                    center: ['50%', '60%'],
                    data:[
                    {value:100, name:'第一类人' + '(' + (100/2230 * 100).toFixed(0)+'%)'},
                    {value:300, name:'第二类人' + '(' + (200/2230 * 100).toFixed(0)+'%)'},
                    {value:400, name:'第三类人' + '(' + (400/2230 * 100).toFixed(0)+'%)'},
                    {value:400, name:'第四类人' + '(' + (400/2230 * 100).toFixed(0)+'%)'},
                    {value:300, name:'第五类人' + '(' + (300/2230 * 100).toFixed(0)+'%)'},
                    {value:250, name:'第六类人' + '(' + (250/2230 * 100).toFixed(0)+'%)'},
                    {value:200, name:'第七类人' + '(' + (200/2230 * 100).toFixed(0)+'%)'},
                    {value:180, name:'第八类人' + '(' + (180/2230 * 100).toFixed(0)+'%)'},
                    {value:100, name:'第九类人' + '(' + (100/2230 * 100).toFixed(0)+'%)'}
                    ]
                  }
                  ]
                };
                var chartDiv = document.getElementById('chart');
                chart.style.left = e.event.clientX + 30 + "px";
                chart.style.top = e.event.clientY - 210/2 + "px";
                myChart.setOption(option);
                $('#chart').css('z-index',2);
              }
            }
          });
          zr.addShape(shape);
        });
zr.render();
});
});

function randomColor(){
  return '#'+('00000'+(Math.random()*0x1000000<<0).toString(16)).substr(-6);
}
function convertPoint (point) {
  return point;
}
</script>
</body>
</html>

FuJie
50 声望24 粉丝

Hello EcmaScript!