导语
大数据呈现应用越来越广泛,支持大数据呈现的SDK,水平较高的有echarts、highchart、D3;然而在地图呈现的功能上,大都只能绘制矢量地图,而不能呈现具有真实效果的地图;鉴于此,本文重点在于如何制作一张,即可以看到真实效果,又能进行交互的矢量地图;
先睹为快
若有所思
技术选择
想实现上述效果,最先想到的SDK是TWaver,思路也非常的简单;
使用Node呈现一张地图背景图片,像素越大越好,缩放效果好;
使用ShapeNode加载地图数据,并设置好位置、缩放比例等因素,恰好与地图重叠;
3.控制地图的Layer为底层,不可选中;ShapeNode为上层,可交互;
iChart & ZRender
本文使用ichart + zrender技术,绘制上述的效果;
为什么使用zrender呢?实际上zrender的功能比较简单,用于绘制基本的形状;其实细心的你会发现,echarts的底层就是使用了zrender;
有为什么使用ichart呢?ichart用于绘制常用的图表,底层基于Canvas绘制,也比较容易改造;而echarts使用的是SVG,修改起来就没那么容易啦!
实验天地
目标一:实现柱单节状图效果
实现柱状图效果,还真没那么容易!ichart不支持怎么办? 定制!
找到ichart的柱状图类:Cylinder.js,好就从改造他开始了!
找到绘制网元的方法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);
最后附上完整代码:
<!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>
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。