D3.js学习笔记九:打包折线图作为类库

2015年02月15日 javascript 暂无评论 阅读 414 views 次

要解决的问题

可能需要在画布上添加多个折线图,并使它们可以进行数据交互,那么要求折线图部件是可复用的,因此,有必要将它打包成为一个类,这样用户在添加折线图的时候就无须了解内部运作机制,直接调用类就可以了。

打包折线图类

将折线图打包成一个名为magicdataLine的类,这次算是比较正式的进行代码编写,因此改动幅度相当大,将代码的逻辑重新梳理了一下,并且摒弃掉了一些无用的环节,修正了折线使用透明颜色导致后面圆点获取不到鼠标的BUG,具体的代码比较长,直接看源文件吧。

察看新的动画演示效果:

<!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}
.tiptools
{
display:block;
background-color:#FFF;
border:1px solid #E7E7E7;
padding:10px;
font-size:12px;
font-family:Arial, Helvetica, sans-serif;
cursor:pointer;
}
</style>
<body>
<script type="text/javascript">
//Magic-data折线图类
function magicdataLine(x,y)
{
//属性列表
this.dataset=[12,25,56,67,12,44,67,10];
this.xMarks=[1,2,3,4,5,6,7,8];
this.lineNames=[];
this.lineColor=["#F00","#09F","#0F0"];
this.x=x;
this.y=y;
this.w=600;
this.h=400;
this.title="Magic-data折线图";
this.subTitle="示例副标题";
//折线图部件
var titleBar=null;
var subTitleBar=null;
var legend=null;
//作图中间变量定义
var lines=[];
var currentLineNum=0;
var padding=40;
var duration=1000;
var head_height=padding;
var foot_height=padding;
var svg=null;
//类的别名引用,在闭包中使用它引用类
var that = this;
//定义画布
svg=d3.select("body")
.append("svg")
.attr("viewBox",-this.x+","+(-this.y) +","+this.w+","+this.h)
.attr("y",this.y)
.attr("width",this.w)
.attr("height",this.h);
//添加背景
svg.append("g")
.append("rect")
.attr("width",this.w)
.attr("height",this.h)
.style("fill","#FFF")
.style("stroke-width",2)
.style("stroke","#E7E7E7");
maxdata=getMaxdata(that.dataset);
//横坐标轴比例尺
var xScale = d3.scale.linear()
.domain([0,this.dataset[0].length-1])
.range([padding,that.w-padding]);
//纵坐标轴比例尺
var yScale = d3.scale.linear()
.domain([0,maxdata])
.range([that.h-foot_height,head_height]);
//定义横轴网格线
var xInner = d3.svg.axis()
.scale(xScale)
.tickSize(-(that.h-head_height-foot_height),0,0)
.tickFormat("")
.orient("bottom")
.ticks(that.dataset[0].length);
//添加横轴网格线
var xInnerBar=svg.append("g")
.attr("class","inner_line")
.attr("transform", "translate(0," + (that.h - padding) + ")")
.call(xInner);
//定义纵轴网格线
var yInner = d3.svg.axis()
.scale(yScale)
.tickSize(-(that.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(that.dataset[0].length);
//添加横坐标轴
var xBar=svg.append("g")
.attr("class","axis")
.attr("transform", "translate(0," + (that.h - foot_height) + ")")
.call(xAxis);
//定义纵轴
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.ticks(10);
//添加纵轴
var yBar=svg.append("g")
.attr("class", "axis")
.attr("transform", "translate("+padding+",0)");
//带有参数的更新函数
this.changeData=function(dataset)
{
this.dataset=dataset;
this.draw();
}
//开始作图
this.draw=function()
{
//判断是否多维数组,如果不是,则转为多维数组,这些处理是为了处理外部传递的参数设置的,现在数据标准,没什么用
if(!(this.dataset[0] instanceof Array))
{
var tempArr=[];
tempArr.push(dataset);
this.dataset=tempArr;
}
head_height=padding;
foot_height=padding;
//如果多条折线,则显示图例
if(this.dataset.length>1)
{
if(legend==null) legend=svg.append("g");
this.addLegend();
foot_height+=25;
}
else
{
if(legend!=null)
{
legend.remove();
legend=null;
}
}
//添加标题
if(this.title!="")
{
if(titleBar==null)
{
titleBar=svg.append("g").append("text")
}
titleBar.text(this.title)
.attr("class","title")
.attr("x",this.w/2)
.attr("y",head_height);
head_height+=30;
}
else
{
if(titleBar!=null) titleBar.remove();
}
//添加副标题
if(this.subTitle!="")
{
if(subTitleBar==null)
{
subTitleBar=svg.append("g").append("text");
}
subTitleBar.text(this.subTitle)
.attr("class","subTitle")
.attr("x",this.w/2)
.attr("y",head_height);
head_height+=20;
}
else
{
if(subTitleBar!=null) subTitleBar.remove();
}
maxdata=getMaxdata(this.dataset);
//添加折线
var lineObject=null;
for(i=0;i<this.dataset.length;i++)
{
if(i<currentLineNum)
{
//对已有的线条做动画
lineObject=lines[i];
lineObject.lineName=this.lineNames[i];
lineObject.xMarks=this.xMarks;
lineObject.setData(i,this.dataset[i],xScale,yScale,this.lineColor[i]);
}
else
{
//如果现有线条不够,就加上一些
var newLine=new CrystalLineObject();
newLine.group=svg.append("g");
newLine.lineName=this.lineNames[i];
newLine.xMarks=this.xMarks;
newLine.setData(i,this.dataset[i],xScale,yScale,this.lineColor[i]);
//侦听圆点点击事件
newLine.addListener("click",function(msg){
alert("系列名称:"+msg.lineName+"\r\n标签:"+msg.xMark+"\r\n值:"+msg.value);
});
lines.push(newLine);
}
}
//删除多余的线条,如果有的话
if(this.dataset.length<currentLineNum)
{
for(i=this.dataset.length;i<currentLineNum;i++)
{
lineObject=lines[i];
lineObject.remove();
}
lines.splice(this.dataset.length,currentLineNum-this.dataset.length);
}
maxdata=getMaxdata(this.dataset);
newLength=this.dataset[0].length;
//横轴数据动画
xScale.domain([0,newLength-1]).range([padding,that.w-padding]);
xAxis.scale(xScale).ticks(newLength);
xBar.transition().duration(duration).attr("transform", "translate(0," + (that.h - foot_height) + ")").call(xAxis);
xBar.selectAll("text").text(function(d){return that.xMarks[d];});
xInner.scale(xScale).ticks(newLength).tickSize(-(that.h-head_height-foot_height),0,0);
xInnerBar.transition().duration(duration).attr("transform", "translate(0," + (that.h - foot_height) + ")").call(xInner);
//纵轴数据动画
yScale.domain([0,maxdata]).range([that.h-foot_height,head_height]);
yBar.transition().duration(duration).call(yAxis);
yInnerBar.transition().duration(duration).call(yInner);
//通过编号获取对应的横轴标签
xBar.selectAll("text")
.text(function(d){return that.xMarks[d];});
//开始线条动画
for(i=0;i<lines.length;i++)
{
lineObject=lines[i];
lineObject.showMovie(duration);
}
currentLineNum=this.dataset.length;
}
//添加图例
this.addLegend=function()
{
var textGroup=legend.selectAll("text")
.data(this.lineNames);
textGroup.exit().remove();
legend.selectAll("text")
.data(this.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 that.lineColor[i];});
var rectGroup=legend.selectAll("rect")
.data(this.lineNames);
rectGroup.exit().remove();
legend.selectAll("rect")
.data(this.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 that.lineColor[i];});
legend.attr("transform","translate("+((this.w-this.lineNames.length*100)/2)+","+(this.h-10)+")");
}
//取得多维数组最大值
function getMaxdata(arr)
{
maxdata=0;
for(i=0;i<arr.length;i++)
{
maxdata=d3.max([maxdata,d3.max(arr[i])]);
}
return maxdata;
}
}
//定义折线类
function CrystalLineObject()
{
//中间的作图变量
this.group=null;
this.lineName="";
this.xMarks=[];
this.ID=-1;
var path=null;
var lineColor="#F00";
var newData=[];
var oldData=[];
var xScale=null;
var yScale=null;
//定义折线的点击事件
var listeners = {};
//类的别名引用,在闭包中使用它引用类
var that=this;
//如果有侦听,则触发事件
var fireEvent= function (eventName, eventProperties)
{
if (!listeners[eventName])
return;
for (var i = 0; i < listeners[eventName].length; i++) {
listeners[eventName][i](eventProperties);
}
};
//注册侦听到类
this.addListener= function (eventName, callback)
{
if (!listeners[eventName])
listeners[eventName] = [];
listeners[eventName].push(callback);
};
//从类里面移除侦听
this.removeListener = function (eventName, callback)
{
if (!listeners[eventName])
return;
for (var i = 0; i < listeners[eventName].length; i++) {
if (listeners[eventName][i] == callback) {
delete listeners[eventName][i];
return;
}
}
};
//接收新数据,初始化折线
this.setData=function(id,arr,xs,ys,color)
{
this.ID=id;
lineColor=color;
var line= d3.svg.line();
//补足/删除路径
if(xScale==null || yScale==null)
{
line.x(function(d,i){return xs(i);})
.y(function(d,i){return ys(arr.length-1);});
}
else
{
line.x(function(d,i){if(i>=oldData.length) return xScale(oldData.length-1); else return xScale(i);})
.y(function(d,i){if(i>=oldData.length) return yScale(0); else return yScale(oldData[i]);});
}
//路径初始化
if(path==null) path=this.group.append("path");
path.attr("d",line(arr))
.style("fill","none")
.style("stroke-width",1)
.style("stroke",lineColor)
.style("stroke-opacity",0.9);
//截断旧数据
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("r",5)
.attr("fill",lineColor)
.attr("cursor","pointer");
if(xScale==null || yScale==null)
{
//第一次进入,直接写折线
this.group.selectAll("circle")
.attr("cx",function(d,i){return xs(i);})
.attr("cy",function(d,i){return ys(arr.length-1);});
}
else
{
//根据旧的坐标轴摆放圆点
this.group.selectAll("circle")
.attr("cx", function(d,i){
if(i>=oldData.length) return xScale(oldData.length-1); else return xScale(i);
})
.attr("cy",function(d,i){
if(i>=oldData.length) return yScale(0); else return yScale(oldData[i]);
})
}
//为圆点添加行为
this.group.selectAll("circle")
.on("mouseover",function(d,i){
var tx=parseFloat(d3.select(this).attr("cx"));
var ty=parseFloat(d3.select(this).attr("cy"));
var tips=that.group.append("g")
.attr("id","tips");
var tipRect=tips.append("rect")
.attr("x",tx+10)
.attr("y",ty+10)
.attr("width",120)
.attr("height",30)
.attr("fill","#FFF")
.attr("stroke-width",1)
.attr("stroke","#CCC");
var tipText=tips.append("text")
.attr("class","tiptools")
.text(that.lineName+"\r\n"+that.xMarks[i]+"\r\n"+d)
.attr("x",tx+20)
.attr("y",ty+30);
})
.on("mouseout",function(d,i){
d3.select("#tips").remove();
})
.on("click",function(d,i)
{
fireEvent("click",{lineName:that.lineName,xMark:that.xMarks[i],value:d});
});
xScale=xs;
yScale=ys;
newData=arr;
};
//重绘加动画效果
this.showMovie=function(_duration)
{
var line = d3.svg.line()
.x(function(d,i){return xScale(i);})
.y(function(d){return yScale(d);});
//路径动画
path.transition().duration(_duration).attr("d",line(newData));
//圆点动画
this.group.selectAll("circle")
.transition()
.duration(_duration)
.attr("cx", function(d,i) {
return xScale(i);
})
.attr("cy", function(d) {
return yScale(d);
})
oldData=newData;
};
//从画布删除折线
this.remove=function()
{
this.group.remove();
};
}
//产生随机数据
function getData()
{
var lineNum=Math.round(Math.random()*10)%3+1;
var dataNum=Math.round(Math.round(Math.random()*10))+5;
var dataset=[];
var xMarks=[];
var 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()*400));
}
dataset.push(tempArr);
lineNames.push("系列"+i);
}
return {dataset:dataset,xMarks:xMarks,lineNames:lineNames};
}
//测试图表的代码
var chart=new magicdataLine(10,10);
var chartData=getData();
chart.dataset=chartData.dataset;
chart.xMarks=chartData.xMarks;
chart.lineNames=chartData.lineNames;
chart.draw();
//测试数据变换
function changeData()
{
var chartData=getData();
chart.dataset=chartData.dataset;
chart.xMarks=chartData.xMarks;
chart.lineNames=chartData.lineNames;
chart.draw();
}
</script>
<p align="left">
<button onClick="javascript:changeData();">刷新数据</button>
</p>
</body>
</html>

,打开后右键查看源码。点击【更新数据】按钮,折线图数据变化。

实现向下钻取

我们为折线图本身添加Click事件,在外部侦听它,当折线图被点击的时候,用它钻取的数据更新另外一个折线图。为折线图添加事件响应和为折线添加响应一样,我们用如下的代码测试数据,定义两个图表,当第一个折线点击的时候,触发Click事件,这时从第一张图发送选定的数据系列,我们将它显示在第二张图上。

//测试图表的代码
var chart=new magicdataLine(10,10);
var chartData=getData();
chart.title="源数据折线图";
chart.dataset=chartData.dataset;
chart.xMarks=chartData.xMarks;
chart.lineNames=chartData.lineNames;
chart.draw();
chart.addListener("click",function(msg)
{
chart2.title="目标数据折线图";
chart2.dataset=[msg.selectData];
chart2.xMarks=chartData.xMarks;
chart2.lineNames=chartData.lineNames;
chart2.draw();
});var chart2=new magicdataLine(10,10);

//测试数据变换
function changeData()
{
var chartData=getData();
chart.dataset=chartData.dataset;
chart.xMarks=chartData.xMarks;
chart.lineNames=chartData.lineNames;
chart.draw();
}

主要是chart2.dataset=[msg.selectData];这一句,我们将选定的数据系列转为二维数组,发送到第二张图,然后重绘。现在我们的折线图如下图所示,我们选定了红色数据系列,它在右侧同样显示了。

9_2

察看新的动画演示效果:demo9_2.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 © 大一网 保留所有权利.  

用户登录

分享到: