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<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)+")");
}
這個是常規的功能,代碼雖然多,但是不難看懂,現在我們的折線圖如下圖所示。
察看新的動畫演示效果:
| ? | <!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学习笔记七:多系列折线图与图例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [附源码]SSM计算机毕业设计构建养猪场
- 下一篇: C++程序设计原理与实践(C++之父最作