D3.js学习笔记七:多系列折线图与图例

2015年02月15日 javascript 暂无评论 阅读 1,792 views 次

要解决的问题

现在这个统计图还要解决几个问题:支持多个系列、为多系列加入图例。

现在的数据是单条折线,如果有多条折线,那么需要为它指定不同的名称和颜色,为它们指定图例,指定图例以后,我们通过图例来控制折线的显示和隐藏。

通过多维数组产生折线

首先调整产生数据系列的函数,使它产生不定长度的随机数,每一个系列为一个数组,并指定折线名称。

//产生随机数据
function getData()
{
var lineNum=Math.round(Math.random()*10)%3+1;
var dataNum=Math.round(Math.round(Math.random()*10))+5;
oldData=dataset;
dataset=[];
xMarks=[];
lineNames=[];for(i=0;i<dataNum;i++)
{
xMarks.push("标签"+i);
}
for(i=0;i<lineNum;i++)
{
var tempArr=[];
for(j=1;j<dataNum;j++)
{
tempArr.push(Math.round(Math.random()*h));
}
dataset.push(tempArr);
lineNames.push("系列"+i);
}
}

我们希望能够自由的添加折线,最好的办法就是将折线封装起来,做成一个折线类,每次添加删除就调用它的相关方法就行了,定义折线类,它有4个方法,init是第一次产生折线时候调用,它初始化内部对象,movieBegin在数据更换之前调用,将图表置于动画开始状态,reDraw开始数据动画,remove将折线从画布清除。

//定义折线类
function CrystalLineObject()
{
this.group=null;
this.path=null;
this.oldData=[];this.init=function(id)
{
var arr=dataset[id];
this.group=svg.append("g");var line = d3.svg.line()
.x(function(d,i){return xScale(i);})
.y(function(d){return yScale(d);});

//添加折线
this.path=this.group.append("path")
.attr("d",line(arr))
.style("fill","none")
.style("stroke-width",1)
.style("stroke",lineColor[id])
.style("stroke-opacity",0.9);

//添加系列的小圆点
this.group.selectAll("circle")
.data(arr)
.enter()
.append("circle")
.attr("cx", function(d,i) {
return xScale(i);
})
.attr("cy", function(d) {
return yScale(d);
})
.attr("r",5)
.attr("fill",lineColor[id]);
this.oldData=arr;
};

//动画初始化方法
this.movieBegin=function(id)
{
var arr=dataset[i];
//补足/删除路径
var olddata=this.oldData;
var line= d3.svg.line()
.x(function(d,i){if(i>=olddata.length) return w-padding; else return xScale(i);})
.y(function(d,i){if(i>=olddata.length) return h-foot_height; else return yScale(olddata[i]);});

//路径初始化
this.path.attr("d",line(arr));

//截断旧数据
var tempData=olddata.slice(0,arr.length);
var circle=this.group.selectAll("circle").data(tempData);

//删除多余的圆点
circle.exit().remove();

//圆点初始化,添加圆点,多出来的到右侧底部
this.group.selectAll("circle")
.data(arr)
.enter()
.append("circle")
.attr("cx", function(d,i){
if(i>=olddata.length) return w-padding; else return xScale(i);
})
.attr("cy",function(d,i){
if(i>=olddata.length) return h-foot_height; else return yScale(d);
})
.attr("r",5)
.attr("fill",lineColor[id]);

this.oldData=arr;
};

//重绘加动画效果
this.reDraw=function(id,_duration)
{
var arr=dataset[i];
var line = d3.svg.line()
.x(function(d,i){return xScale(i);})
.y(function(d){return yScale(d);});

//路径动画
this.path.transition().duration(_duration).attr("d",line(arr));

//圆点动画
this.group.selectAll("circle")
.transition()
.duration(_duration)
.attr("cx", function(d,i) {
return xScale(i);
})
.attr("cy", function(d) {
return yScale(d);
})
};

//从画布删除折线
this.remove=function()
{
this.group.remove();
};
}

我们修改了drawChart()函数,使得它针对不定数量的折线作出处理,如果少了,就加上,否则删除多余的线条。

for(i=0;i<dataset.length;i++)
{
if(i<currentLineNum)
{
//对已有的线条做动画
lineObject=lines[i];
lineObject.movieBegin(i);
}
else
{
//如果现有线条不够,就加上一些
var newLine=new CrystalLineObject();
newLine.init(i);
lines.push(newLine);
}
}//删除多余的线条,如果有的话
if(dataset.length<currentLineNum)
{
for(i=dataset.length;i<currentLineNum;i++)
{
lineObject=lines[i];
lineObject.remove();
}
lines.splice(dataset.length,currentLineNum-dataset.length);
}

为系列添加图例

我们添加一个图例元素到画布,并且将图例的增删改做成了一个函数,代码如下:

//添加图例
function addLegend()
{
var textGroup=legend.selectAll("text")
.data(lineNames);textGroup.exit().remove();legend.selectAll("text")
.data(lineNames)
.enter()
.append("text")
.text(function(d){return d;})
.attr("class","legend")
.attr("x", function(d,i) {return i*100;})
.attr("y",0)
.attr("fill",function(d,i){ return lineColor[i];});

var rectGroup=legend.selectAll("rect")
.data(lineNames);

rectGroup.exit().remove();

legend.selectAll("rect")
.data(lineNames)
.enter()
.append("rect")
.attr("x", function(d,i) {return i*100-20;})
.attr("y",-10)
.attr("width",12)
.attr("height",12)
.attr("fill",function(d,i){ return lineColor[i];});

legend.attr("transform","translate("+((w-lineNames.length*100)/2)+","+(h-10)+")");
}

这个是常规的功能,代码虽然多,但是不难看懂,现在我们的折线图如下图所示。

7_1

察看新的动画演示效果:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>画一个折线图</title>
<script type="text/javascript" src="js/d3.js"></script>
</head>
<style type="text/css">
body{
height: 100%;
}
.title{font-family:Arial,微软雅黑;font-size:18px;text-anchor:middle;}
.subTitle{font-family:Arial,宋体;font-size:12px;text-anchor:middle;fill:#666}
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 11px;
fill:#999;
}
.inner_line path,
.inner_line line {
fill: none;
stroke:#E7E7E7;
shape-rendering: crispEdges;
}
.legend{font-size: 12px; font-family:Arial, Helvetica, sans-serif}
</style>
<body>
<script type="text/javascript">
var dataset=[];
var lines=[]; //保存折线图对象
var xMarks=[];
var lineNames=[]; //保存系列名称
var lineColor=["#F00","#09F","#0F0"];
var w=600;
var h=400;
var padding=40;
var currentLineNum=0;
//用一个变量存储标题和副标题的高度,如果没有标题什么的,就为0
var head_height=padding;
var title="收支平衡统计图";
var subTitle="2013年1月 至 2013年6月";
//用一个变量计算底部的高度,如果不是多系列,就为0
var foot_height=padding;
//模拟数据
getData();
//判断是否多维数组,如果不是,则转为多维数组,这些处理是为了处理外部传递的参数设置的,现在数据标准,没什么用
if(!(dataset[0] instanceof Array))
{
var tempArr=[];
tempArr.push(dataset);
dataset=tempArr;
}
//保存数组长度,也就是系列的个数
currentLineNum=dataset.length;
//图例的预留位置
foot_height+=25;
//定义画布
var svg=d3.select("body")
.append("svg")
.attr("width",w)
.attr("height",h);
//添加背景
svg.append("g")
.append("rect")
.attr("x",0)
.attr("y",0)
.attr("width",w)
.attr("height",h)
.style("fill","#FFF")
.style("stroke-width",2)
.style("stroke","#E7E7E7");
//添加标题
if(title!="")
{
svg.append("g")
.append("text")
.text(title)
.attr("class","title")
.attr("x",w/2)
.attr("y",head_height);
head_height+=30;
}
//添加副标题
if(subTitle!="")
{
svg.append("g")
.append("text")
.text(subTitle)
.attr("class","subTitle")
.attr("x",w/2)
.attr("y",head_height);
head_height+=20;
}
maxdata=getMaxdata(dataset);
//横坐标轴比例尺
var xScale = d3.scale.linear()
.domain([0,dataset[0].length-1])
.range([padding,w-padding]);
//纵坐标轴比例尺
var yScale = d3.scale.linear()
.domain([0,maxdata])
.range([h-foot_height,head_height]);
//定义横轴网格线
var xInner = d3.svg.axis()
.scale(xScale)
.tickSize(-(h-head_height-foot_height),0,0)
.tickFormat("")
.orient("bottom")
.ticks(dataset[0].length);
//添加横轴网格线
var xInnerBar=svg.append("g")
.attr("class","inner_line")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xInner);
//定义纵轴网格线
var yInner = d3.svg.axis()
.scale(yScale)
.tickSize(-(w-padding*2),0,0)
.tickFormat("")
.orient("left")
.ticks(10);
//添加纵轴网格线
var yInnerBar=svg.append("g")
.attr("class", "inner_line")
.attr("transform", "translate("+padding+",0)")
.call(yInner);
//定义横轴
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(dataset[0].length);
//添加横坐标轴
var xBar=svg.append("g")
.attr("class","axis")
.attr("transform", "translate(0," + (h - foot_height) + ")")
.call(xAxis);
//通过编号获取对应的横轴标签
xBar.selectAll("text")
.text(function(d){return xMarks[d];});
//定义纵轴
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.ticks(10);
//添加纵轴
var yBar=svg.append("g")
.attr("class", "axis")
.attr("transform", "translate("+padding+",0)")
.call(yAxis);
//添加图例
var legend=svg.append("g");
addLegend();
//添加折线
lines=[];
for(i=0;i<currentLineNum;i++)
{
var newLine=new CrystalLineObject();
newLine.init(i);
lines.push(newLine);
}
//重新作图
function drawChart()
{
var _duration=1000;
getData();
addLegend();
//设置线条动画起始位置
var lineObject=new CrystalLineObject();
for(i=0;i<dataset.length;i++)
{
if(i<currentLineNum)
{
//对已有的线条做动画
lineObject=lines[i];
lineObject.movieBegin(i);
}
else
{
//如果现有线条不够,就加上一些
var newLine=new CrystalLineObject();
newLine.init(i);
lines.push(newLine);
}
}
//删除多余的线条,如果有的话
if(dataset.length<currentLineNum)
{
for(i=dataset.length;i<currentLineNum;i++)
{
lineObject=lines[i];
lineObject.remove();
}
lines.splice(dataset.length,currentLineNum-dataset.length);
}
maxdata=getMaxdata(dataset);
newLength=dataset[0].length;
//横轴数据动画
xScale.domain([0,newLength-1]);
xAxis.scale(xScale).ticks(newLength);
xBar.transition().duration(_duration).call(xAxis);
xBar.selectAll("text").text(function(d){return xMarks[d];});
xInner.scale(xScale).ticks(newLength);
xInnerBar.transition().duration(_duration).call(xInner);
//纵轴数据动画
yScale.domain([0,maxdata]);
yBar.transition().duration(_duration).call(yAxis);
yInnerBar.transition().duration(_duration).call(yInner);
//开始线条动画
for(i=0;i<lines.length;i++)
{
lineObject=lines[i];
lineObject.reDraw(i,_duration);
}
currentLineNum=dataset.length;
dataLength=newLength;
}
//定义折线类
function CrystalLineObject()
{
this.group=null;
this.path=null;
this.oldData=[];
this.init=function(id)
{
var arr=dataset[id];
this.group=svg.append("g");
var line = d3.svg.line()
.x(function(d,i){return xScale(i);})
.y(function(d){return yScale(d);});
//添加折线
this.path=this.group.append("path")
.attr("d",line(arr))
.style("fill","none")
.style("stroke-width",1)
.style("stroke",lineColor[id])
.style("stroke-opacity",0.9);
//添加系列的小圆点
this.group.selectAll("circle")
.data(arr)
.enter()
.append("circle")
.attr("cx", function(d,i) {
return xScale(i);
})
.attr("cy", function(d) {
return yScale(d);
})
.attr("r",5)
.attr("fill",lineColor[id]);
this.oldData=arr;
};
//动画初始化方法
this.movieBegin=function(id)
{
var arr=dataset[i];
//补足/删除路径
var olddata=this.oldData;
var line= d3.svg.line()
.x(function(d,i){if(i>=olddata.length) return w-padding; else return xScale(i);})
.y(function(d,i){if(i>=olddata.length) return h-foot_height; else return yScale(olddata[i]);});
//路径初始化
this.path.attr("d",line(arr));
//截断旧数据
var tempData=olddata.slice(0,arr.length);
var circle=this.group.selectAll("circle").data(tempData);
//删除多余的圆点
circle.exit().remove();
//圆点初始化,添加圆点,多出来的到右侧底部
this.group.selectAll("circle")
.data(arr)
.enter()
.append("circle")
.attr("cx", function(d,i){
if(i>=olddata.length) return w-padding; else return xScale(i);
})
.attr("cy",function(d,i){
if(i>=olddata.length) return h-foot_height; else return yScale(d);
})
.attr("r",5)
.attr("fill",lineColor[id]);
this.oldData=arr;
};
//重绘加动画效果
this.reDraw=function(id,_duration)
{
var arr=dataset[i];
var line = d3.svg.line()
.x(function(d,i){return xScale(i);})
.y(function(d){return yScale(d);});
//路径动画
this.path.transition().duration(_duration).attr("d",line(arr));
//圆点动画
this.group.selectAll("circle")
.transition()
.duration(_duration)
.attr("cx", function(d,i) {
return xScale(i);
})
.attr("cy", function(d) {
return yScale(d);
})
};
//从画布删除折线
this.remove=function()
{
this.group.remove();
};
}
//添加图例
function addLegend()
{
var textGroup=legend.selectAll("text")
.data(lineNames);
textGroup.exit().remove();
legend.selectAll("text")
.data(lineNames)
.enter()
.append("text")
.text(function(d){return d;})
.attr("class","legend")
.attr("x", function(d,i) {return i*100;})
.attr("y",0)
.attr("fill",function(d,i){ return lineColor[i];});
var rectGroup=legend.selectAll("rect")
.data(lineNames);
rectGroup.exit().remove();
legend.selectAll("rect")
.data(lineNames)
.enter()
.append("rect")
.attr("x", function(d,i) {return i*100-20;})
.attr("y",-10)
.attr("width",12)
.attr("height",12)
.attr("fill",function(d,i){ return lineColor[i];});
legend.attr("transform","translate("+((w-lineNames.length*100)/2)+","+(h-10)+")");
}
//产生随机数据
function getData()
{
var lineNum=Math.round(Math.random()*10)%3+1;
var dataNum=Math.round(Math.round(Math.random()*10))+5;
oldData=dataset;
dataset=[];
xMarks=[];
lineNames=[];
for(i=0;i<dataNum;i++)
{
xMarks.push("标签"+i);
}
for(i=0;i<lineNum;i++)
{
var tempArr=[];
for(j=1;j<dataNum;j++)
{
tempArr.push(Math.round(Math.random()*h));
}
dataset.push(tempArr);
lineNames.push("系列"+i);
}
}
//取得多维数组最大值
function getMaxdata(arr)
{
maxdata=0;
for(i=0;i<arr.length;i++)
{
maxdata=d3.max([maxdata,d3.max(arr[i])]);
}
return maxdata;
}
</script>
<p align="left">
<button onClick="javascript:drawChart();">刷新数据</button>
</p>
</body>
</html>

,打开后右键查看源码,点击【刷新数据】可以看到新的动画效果,加入了多系列支持和图例,看起来比较接近水晶易表的折线图了。

 

参考网站与相关文档资料


D3.js网站:http://d3js.org/

官方API说明https://github.com/mbostock/d3/wiki/API-Reference

学习资料:http://www.dashingd3js.com/

http://bost.ocks.org/mike/

https://github.com/mbostock/d3/wiki

https://groups.google.com/forum/?fromgroups#!forum/d3-js

http://stackoverflow.com/questions/tagged/d3.js

https://github.com/mbostock/d3/wiki/Gallery

给我留言

您必须 登录 才能发表留言!

Copyright © 大一网 保留所有权利.  

用户登录

分享到: