http://www.daliane.com/d3_js_xue_xi_bi_ji_qi_duo_xi_lie_zhe_xian_tu_yu_tu_li/
要解决的问题
现在这个统计图还要解决几个问题:支持多个系列、为多系列加入图例。
现在的数据是单条折线,如果有多条折线,那么需要为它指定不同的名称和颜色,为它们指定图例,指定图例以后,我们通过图例来控制折线的显示和隐藏。
通过多维数组产生折线
首先调整产生数据系列的函数,使它产生不定长度的随机数,每一个系列为一个数组,并指定折线名称。
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()函数,使得它针对不定数量的折线作出处理,如果少了,就加上,否则删除多余的线条。
{
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)+")");
}
这个是常规的功能,代码虽然多,但是不难看懂,现在我们的折线图如下图所示。
察看新的动画演示效果:
<!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>
,打开后右键查看源码,点击【刷新数据】可以看到新的动画效果,加入了多系列支持和图例,看起来比较接近水晶易表的折线图了