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


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 {
xMarks.push("标签"+i);
}
for(i=0;i {
var tempArr=[];
for(j=1;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 {
if(i {
//对已有的线条做动画
lineObject=lines[i];
lineObject.movieBegin(i);
}
else
{
//如果现有线条不够,就加上一些
var newLine=new CrystalLineObject();
newLine.init(i);
lines.push(newLine);
}
}//删除多余的线条,如果有的话
if(dataset.length {
for(i=dataset.length;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)+")");
}

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

察看新的动画演示效果:

 
 
 
  charset="utf-8">
  </span>画一个折线图<span class="html-tag" style="border:0px;">
  type="text/javascriptsrc="js/d3.js">
 
  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}
   
 
 
  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
  {
  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
  {
  if(i
  {
  //对已有的线条做动画
  lineObject=lines[i];
  lineObject.movieBegin(i);
  }
  else
  {
  //如果现有线条不够,就加上一些
  var newLine=new CrystalLineObject();
  newLine.init(i);
  lines.push(newLine);
  }
  }
   
  //删除多余的线条,如果有的话
  if(dataset.length
  {
  for(i=dataset.length;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
  {
  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
  {
  xMarks.push("标签"+i);
  }
  for(i=0;i
  {
  var tempArr=[];
  for(j=1;j
  {
  tempArr.push(Math.round(Math.random()*h));
  }
  dataset.push(tempArr);
  lineNames.push("系列"+i);
  }
  }
   
  //取得多维数组最大值
  function getMaxdata(arr)
  {
  maxdata=0;
  for(i=0;i
  {
  maxdata=d3.max([maxdata,d3.max(arr[i])]);
  }
  return maxdata;
  }
 
  align="left">
  onClick="javascript:drawChart();">刷新数据
 

 
 

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


你可能感兴趣的:(d3.js)