D3.js学习笔记八:折线图的鼠标响应和向下钻取
要解决的问题
我当初为什么选择D3.js?不是因为它的图像多么绚丽,那是作图软件的基本功能,如果只是为了做一些静态图像,那没有必要费劲来写这个代码,选择它,主要是因为它在作图的同时还具有交互性,能够自由操控图像元素,这才是SVG的精华所在。目前为止,已经完成了折线图的绘制工作,接下来为折线图设置交互行为。
为折线图增加鼠标悬停行为
在折线图的圆点上设置鼠标监控事件,当鼠标移动到圆形上,显示为手型并显示提示,显示手型,只需要为圆点添加属性.attr(“cursor”,”pointer”)就可以了。显示提示的话,我们首先为提示文字做一些样式。
{
display:block;
background-color:#FFF;
border:1px solid #E7E7E7;
padding:10px;
font-size:12px;
font-family:Arial, Helvetica, sans-serif;
cursor:pointer;
}
然后侦听圆点的鼠标经过(mouseover)和离开(mouseout)事件,为它们添加响应函数,为了能够在鼠标离开的时候删除提示,我们为提示的Group添加了一个id属性,在鼠标离开时候,使用 d3.select(“#[id]”)这样的语句来定位它。
.on(“mouseover”,function(d,i){
var tx=parseFloat(d3.select(this).attr(“cx”));
var ty=parseFloat(d3.select(this).attr(“cy”));var tips=svg.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(lineNames[id]+”\r\n”+xMarks[i]+”\r\n”+d)
.attr(“x”,tx+20)
.attr(“y”,ty+30);
})
.on(“mouseout”,function(d,i){
d3.select(“#tips”).remove();
});
察看新的动画演示效果:
<!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“> 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(“cursor”,”pointer”) .attr(“r”,5) .attr(“fill”,lineColor[id]); 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=svg.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(lineNames[id]+”\r\n”+xMarks[i]+”\r\n”+d) .attr(“x”,tx+20) .attr(“y”,ty+30); }) .on(“mouseout”,function(d,i){ d3.select(“#tips”).remove(); }); 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]) .attr(“cursor”,”pointer”); 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=svg.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(lineNames[id]+”\r\n”+xMarks[i]+”\r\n”+d) .attr(“x”,tx+20) .attr(“y”,ty+30); }) .on(“mouseout”,function(d,i){ d3.select(“#tips”).remove(); }); 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>
,打开后右键查看源码,鼠标移动到圆点上,可以看到数据提示标签。
实现向下钻取
现在图表已经具有一定的交互性了,我们希望能够在点击圆点的时候,对内部数据有一个选择,并将选择结果发送出来,类似水晶易表的【向下钻取】功能。那么首先需要定义一个事件,当用户点击鼠标,事件被触发,如果有另外的对象侦听它,则将数据发送出来。
var listeners = {};//如果有侦听,则触发事件
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;
}
}
};
接下来,我们在折线点击的时候,触发这个事件。
……
.on(“click”,function(d,i)
{
fireEvent(“click”,{lineName:lineNames[id],xMark:xMarks[i],value:d});
});
然后在主程序的添加折线里面注册侦听,看看用户点击的圆点信息,这里我们简单的alert一下,如果需要和折线图外部交互,还需要将折线图包装一下,做成一个类,然后为它再定义事件,并注册侦听。
lines=[];
for(i=0;i<currentLineNum;i++)
{
var newLine=new CrystalLineObject();
newLine.init(i);
//侦听圆点点击事件
newLine.addListener(“click”,function(msg){
alert(“系列名称:”+msg.lineName+”\r\n标签:”+msg.xMark+”\r\n值:”+msg.value);
});
lines.push(newLine);
}
具体的js自定义事件的解释,网上很多,感兴趣的话,可以深入研究一下,现在我们的折线图如下图所示。
察看新的动画演示效果:demo8_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
