近几年随着大数据逐渐火热,数据可视化也就显得格外重要,Ben Fry在他的著作《Visualiziing Data》中将数据可视化的过程分为七个步骤:
前面4步分别属于数据采集、数据分析、数据处理和数据挖掘领域,数据可视化的处理范围主要是后面三步。
本文是基于实验室做的智能客运公交wifi系统对后台数据进行可视化处理,分析业务需求主要是将新增关注人数、取消关注人数和每车最大连接数以折线图的形式动态展示。
整个数据可视化方案实施近两个星期,前期进行可视化技术选型,由于数据可视化插件种类和质量参差不齐,其中包括echarts(百度开源)http://echarts.baidu.com/,底层基于canvas;highcharts https://www.hcharts.cn/,基于svg,老牌图表库,商业用途需购买版权;还有一个就是D3.js https://d3js.org/可自由设计图表,适合展示丰富多样的图表样式。对于这三种插件进行以下分析:
1、兼容性
使用每个插件之前必须考虑它的兼容性问题,否则项目完成后发现部分浏览器不能用就需要重新选型或解决兼容性问题,事倍功半,得不偿失!
- Highcharts 兼容 IE6 及以上的所有主流浏览器,完美支持移动端缩放、手势操作。
- Echarts 兼容 IE6 及以上的所有主流浏览器,同样支持移动端的缩放和手势操作。
- D3 兼容IE9 及以上的所有主流浏览器,对于移动端的兼容性也同上。
目前这三种插件基本满足兼容性要求。
2、是否开源
3.难易程度
总的来说,我认为Echarts是国内做的最好的图表库,有多样的图表类型,色彩丰富,交互友好,易上手;Highcharts作为老牌图表库,国外开源项目,技术相对成熟,代码有社区人员维护,如果项目中对图表样式和展现效果没有严格要求,可以考虑用以上两种。我第一次接触D3,是在官网上看到的,酷炫的案例让我眼前一亮,马上在网上买了一本《精通D3.js》,学习之后发现做数据可视化是件很有意思的事情。由于秉承着科研精神和工匠精神,加之之前一段时间接触过D3.v3,做过一些demo,多样的图表设计和丰富的展示效果深深吸引着我,所以项目做对后台数据可视化处理我选择D3.js作为开发工具。
完成技术选型之后,接下来就是方案设定,基于智能客运公交wifi系统后台数据可视化输出要求,我画了简单的交互设计稿,如下图所示:
主要有四类数据:新增关注人数、取消关注人数、付费人数和最大连接数,其中最大连接数还需选择车次,这四类可以设计4个控件,选择不同控件会呈现出相应的折线图,车次选择下拉菜单只有在点击最大连接数时才显示。
首先约定数据格式为以对象元素组成的数组类型。如:
dataset=[{x:'2017-11-01',y:20},{x:'2017-11-02',y:18},
{x:'2017-11-03',y:22},{x:'2017-11-04',y:26},
{x:'2017-11-05',y:16},{x:'2017-11-06',y:30},
{x:'2017-11-07',y:22},{x:'2017-11-08',y:28},
{x:'2017-11-09',y:30},{x:'2017-11-10',y:36}]
1、需要构建SVG。SVG可以理解为PS中的画布或者是一张白纸,可以填充任意颜色甚至可以为透明色,代码如下:
svg=d3.select(".container")
.append("svg")
.attr("class","drawSVG");
/* SVG样式 */
.drawSVG{
display: inline-block;
margin-right:10px;
width: 100%;
height:500px;
}
2、定义比例尺。需求中以近十天的日期作为横坐标,因此在做比例尺定义的时候需考虑非线性比例尺,即序列比例尺,定义域和值域一一对应。在定义比例尺之前需计算纵坐标的最大值,代码如下:
var gdpmax=0;
//遍历dataset.y,将字符串转换成数字
//d3.max无法计算字符串大小,因此需要将dataset.y中所对应的字符串转换成数字
var newDataset=[];
for(var i=0;iNumber(dataset[i].y);
newDataset.push(numberY);
}
var currGdp=d3.max(newDataset);
if (currGdp>gdpmax)
gdpmax=currGdp;
用于每次数据获取的过程都需通过AJAX调用后台数据,返回的结果在数字变字符串,在使用d3.max求最大值的时候,字符串如何计算,导致计算错误,因此需要将字符串转换为数字,储存在newDataset数组中,再利用d3.max(newDateset)计算纵坐标最大值。
定义比例尺代码如下:
xScale=d3.scaleOrdinal()//实现需要的非线性比例尺
.domain(date)//data=['2017-11-01','2017-11-02',...]
.range(space);//space=[0,90,180,...]
yScale=d3.scaleLinear()//横坐标比例尺
.domain([0,gdpmax*1.1])
.range([height-padding.top-padding.bottom,0]);
由于利用d3.scaleOrdinal()序列比例尺,定义域与值域一一对应,为了让定义域的个数决定值域个数,我设计了一个小技巧。
var sum=0;
for (var i=0;i90;
}//data=['2017-11-01','2017-11-02',...];space=[0,90,180,...]
space数组的个数有data个数决定,并且间距为90,决定横坐标的单位坐标为90。
3、绘制坐标轴。在把比例尺定义后之后,绘制坐标轴的过程就简单多了,具体代码如下:
/*定义x轴*/
var xAxis=d3.axisBottom(xScale)
.tickFormat(d3.format("d"))
.tickSizeOuter(10)
.tickPadding(10);
/*定义y轴*/
var yAxis=d3.axisLeft(yScale)
.tickSizeOuter(10)
.tickPadding(10);
/*添加一个元素用于放X轴*/
svg.append("g")
.attr("class","axis")
.attr("transform","translate("+padding.left+","+(height-padding.bottom)+")")
.call(d3.axisBottom(xScale));
/*添加一个元素用于放Y轴*/
svg.append("g")
.attr("class","axis")
.attr("transform","translate("+padding.left+","+padding.top+")")
.call(yAxis);
}
4、渲染数据。前面的工作只是在画板上定好绘画区域,接下来解决数据绑定和呈现问题,数据绑定是D3处理选择集和数据的方法,是D3之所以被称之为D3的原因。选择集上是没有数据的,数据绑定就是使选择集“拥有”数据的过程,主要有以下两种方法:
svg中有两个形状元素可以画直线,一个是
,另一个是
。
中需要添加x1
,y1,x2,y2属性,决定线段的起点和终点,但是数据绑定以后,一方面不知道数据值是多少,另一方面手动绑定比较繁琐,所以想利用
和直线生成器,直接生成线段,简单粗暴。
/*创建一个直线生成器*/
var linePath=d3.line()
.x(function (d) {
return xScale(d.x);
})
.y(function (d) {
return yScale(d.y);
});
svg.selectAll("path")
.data(dataset)//绑定dataset数据
.enter()
.append("path")
.attr("d",linePath(dataset))//调用直线生成器
.attr("transform","translate("+padding.left+","+padding.top+")")
.attr("fill","none")
.attr("stroke-width",2)
.attr("stroke",'#F1C40F')
.attr("class","path");
5、设计控件。目前为止,数据可视化模块基本实现,接下来需创建4个控件,监听控件点击事件,并实现相应的数据可视化。html和css相对简单些,代码如下:
class="container">
class="btnGroup">
class="bosNum">
html,body{
height: 100%;
}
*{
margin: 0;
padding:0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.container{
width: 80%;
margin: 0 auto;
border: 1px solid red;
}
/* 按键组样式 */
.btnGroup{
margin: 20px 0 0 20px;
}
/* 按键正常样式 */
button{
width: 100px;
height: 30px;
background: #D35400;
border: 0;
border-radius: 5px;
margin-right: 4px;
color: #fff;
transition: all 0.2s;
outline: 0;
}
/* 鼠标滑过时按键样式 */
button:hover{
cursor: pointer;
background: #E67E22;
border-radius: 5px;
}
/* 按键选中时 */
.active{
background: #F1C40F;
border-radius: 5px;
border: 0;
}
/* 车次选择样式 */
.bosNum{
display: inline-block;
font: 14px "sans-serif";
}
select{
width: 100px;
height: 30px;
}
/*
*新增人数触发事件
*/
addPeople.on("click",function () {
$("svg").remove();
bosNum.hide();//隐藏汽车车次选择
addPeople.addClass("active");
maxLinkNum.removeClass("active");
removePeople.removeClass("active");
key=addPeople.data("index");//获取当前addPeople按键data-index值
//ajax调用后台数据
$.getJSON("newClients",function(data) {
/*
*新建从0到N的数组,为后续做虚假坐标做准备
*/
dataset1=[];
for (var j=0; j//调用构建SVG画布函数
defindScale(data);//调用比例尺和坐标轴函数
drawDATA(data);//调用渲染数据函数
//延迟1s执行获取坐标系中的焦点函数
setTimeout(function () {
drawFocus(data);
},1000);
})
});
6、友好交互。
(1)让折线过渡加载。灵感来源于下图,设计原理利用css中animation动画效果,和path元素中的stroke-dasharray和stroke-dashoffset属性,stroke-dasharray:1000;表示定义虚线中实线和虚线的长度都为1000,先实线后虚线;stroke-dashoffset:1000(stroke-dashoffset的值要大于实现线段的长度!),决定虚线距离原初始位置向左的偏移为1000,再利用animation动画,将stroke-dashoffset:1000减小到stroke-dashoffset:0。
/* 折线过渡动效 */
.path {
stroke-dasharray: 4000;
animation: dash 4s ease-out;
}
@keyframes dash {
0%{
stroke-dashoffset: 4000;
}
100%{
stroke-dashoffset: 0;
}
}
效果如下图所示:
(2)焦点提示。实现当鼠标移动到svg是触发鼠标移动事件,并通过计算得到焦点坐标。代码如下图所示:
svg.append("rect")
.attr("class", "overlay")
.attr("x", padding.left)
.attr("y", padding.top)
.attr("width", space[space.length-1])
.attr("height", height - padding.top - padding.bottom)
/*监听鼠标移入事件*/
.on("mouseover", function () {
focusCircle.style("display", null);
focusLine.style("display", null);
})
/*监听鼠标移出事件*/
.on("mouseout", function () {
focusCircle.style("display", "none");
focusLine.style("display", "none");
})
/*监听鼠标滑动事件*/
.on("mousemove", mousemove);
/*焦点元素*/
var focusCircle = svg.append("g")
.attr("class", "focusCircle")
.style("display", "none");
focusCircle.append("circle")
.attr("r", 4.5);
focusCircle.append("text")
.attr("dx", 10)
.attr("dy", -10);
/*对齐线元素*/
var focusLine = svg.append("g")
.attr("class", "focusLine")
.style("display", "none");
var vLine = focusLine.append("line");
var hLine = focusLine.append("line");
/*鼠标在透明矩形内滑动时调用*/
function mousemove() {
var mouseX=d3.mouse(this)[0]-padding.left;
var mouseY=d3.mouse(this)[1]-padding.top;
var x0=xScale1.invert(mouseX);
x0=Math.round(x0);
/*查找原数组中x0的值,并返回索引号*/
var bisect=d3.bisector(function (d) {
return d;
}).left;
var index=bisect(dataset1,x0);
var x1=dataset[index].x;
var y1=dataset[index].y;
/*分别用x轴和y轴的比例尺,计算焦点的位置*/
var focusX = xScale(x1) + padding.left;
var focusY = yScale(y1) + padding.top;
switch(key){
case 0:
/*设定焦点的文字信息*/
focusCircle.select("text").text(x1 + "新增关注:" + y1 + "人")
.attr("style", "opacity:0.8");
break;
case 1:
/*设定焦点的文字信息*/
focusCircle.select("text").text(x1 + "取消关注:" + y1 + "人")
.attr("style", "opacity:0.8");
break;
default:
/*设定焦点的文字信息*/
focusCircle.select("text").text(x1 + "最大连接数:" + y1 + "人")
.attr("style", "opacity:0.8");
}
/*通过平移,使焦点移动到指定位置*/
focusCircle.attr("transform", "translate(" + focusX + "," + focusY + ")");
/*设定垂直对齐线的起点和终点*/
vLine.attr("x1", focusX)
.attr("y1", focusY)
.attr("x2", focusX)
.attr("y2", height - padding.bottom)
.attr("style", "stroke:#666;stroke-dasharray:10");
/*设定水平对齐线的起点和终点*/
hLine.attr("x1", focusX)
.attr("y1", focusY)
.attr("x2", padding.left)
.attr("y2", focusY)
.attr("style", "stroke:#666;stroke-dasharray:10");
}