数据总是在变化的,那么我们要如何将变化的数据反映到图表上呢?
在D3中,这些变化通过更新进行处理。而过渡通过使用动画用于处理视觉上的展示。
#(1)更新
首先,我们定义一个序数比例尺:
let xScale = d3.scale.ordinal()//定义一个序数比例尺,用于处理序数
注:序数是一些有固定顺序的一些类别,如:
- 新生、大二、大三、毕业班
- B等、A等、AA等
- 非常不喜欢、不喜欢、没感觉、喜欢、非常喜欢
然后,为比例尺设定输入值的值域。在线性比例尺中,用包含两个值的数组来设置值域,如[0,100];而在序数比例尺中,值域是序数,不是线性或定量的数据。
如:
let xScale = d3.scale.ordinal() .domain(["新生","大二","大三","毕业班"])
但是,如果没有明确的类别,我们可以给每个数据点或条形分配一个其在数据集中对应位置的ID值,如0、1、2、3等等。
而在本例子中,我们使用
.domain(d3.range(dataset.length)) //相当于.domain([0,dataset.length]),若dataset.length为3,那么就是[0,1,2]
(2)自动分档
与线性比例尺使用的连续范围值不同,序数比例尺使用的是离散范围值,即输出值是事先确定好的,可以是数值,也可以不是。
在映射范围时,可以使用range()
,也可以使用rangeBands()
。后者接收一个最小值和一个最大值,然后根据输入值域的长度自动将其切分成相等的块域或“档”,如:
.rangeBands([0.w])
//计算从0到w可以均分为几档,然后把比例尺的范围设定为这些“档”,例如有3档,那么w/3为每一档的“宽度”。还可以给rangeBands()传入第二个参数,指定档之间的间距。
rangeRoundBands()
会对输出的值舍入为最接近的整数。如3.1415变成3。整数值有助于将视觉元素与像素网格对齐。
#(3)更新
到目前为止,我们的代码还是随着页面的加载执行。对于更新数据来说,可以在开始的绘制代码一执行完毕就更新,但这样更新太快。为了能看到更新的变化,需要把更新的代码与其他代码分开。因此,需要在页面加载之后添加一个“触发器”,用以触发数据和图表的更新。例如,使用鼠标点击事件。
- 通过事件监听实现交互
首先在body中添加一个p标签,用于点击事件更新图表:
<p>Click on thie text to update the chart</p>
接着在D3代码最后,添加D3的事件监听。
d3.select("p") .on("click",function() {//selection.on()方法是添加事件监听器的简便方法,接受两个参数:事件类型和监听器(匿名函数) //p标签被单击时执行的任务 alert("Hey!"); });
接下来,我们需要改变数据,或者说更新数据。为此,需要:
- 重新绑定新数据与已有元素;
- 选择相应的图形,如散点、矩形,再调用一次data()方法; 例如这里,我们选择散点(圆形)为例:
- 最后更新视觉元素的属性,以反映更新后的数据值
dataset = [ [5,20],[480,90],[250,50],[100,33],[330,95],[410,12],[475,44],[25,67],[85,21],[220,88] ]; svg.selectAll("circle") .data(dataset); //重新绑定新数据
我们将这三步的代码放到事件监听函数里面:
d3.select("p") .on("click",function() {//selection.on()方法是添加事件监听器的简便方法,接受两个参数:事件类型和监听器(匿名函数) //p标签被单击时执行的任务 //新数据集 dataset = [ [5,20],[480,90],[250,50],[100,33],[330,95],[410,12],[475,44],[25,67],[85,21],[220,88] ]; //更新所有散点,注意到这里没有enter()和append() svg.selectAll("circle") .data(dataset) //重新绑定新数据 .attr("cx",function(d,i){ return xScale(d[0]); }) .attr("cy",function(d){ return yScale(d[1]); }) .attr("r",function(d){ return rScale(d[1]); }); });
最后点击p标签执行点击事件,更新数据。当然,如果图表上有标签或者颜色编码,你需要记得一并更新。
- 过渡动画
你是不是觉得更新数据的效果不够炫酷?
那么我们来认识下D3中提供的过渡动画—transition()
要创建一个过渡效果,只需要在更新时简单添加一行代码:
.transition()
但是多少的持续时间是合适的呢?根据经验,细微的界面反馈(如鼠标悬停在元素上触发过渡),过渡时间大约150毫秒较合适,而更显著的视觉过渡(比如整个数据视图的变化)持续1000毫秒较合适。
除此之外,我们还可以设置过渡类型,D3中使用ease()
指定不同的过渡类型,默认的效果的"cublic-in-out
",另外还有"linear"线性类型。
对于ease()的使用,需要再transition()之后、attr()之前指定。当然,除了ease()还有circle()、elastic()、bounce()
等函数用于处理过渡动画。
你可能还想设置动画的开始时间,delay(1000)或delay(function(){})
可以设置。
#(4)完成代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> div.bar { display: inline-block; width: 20px; height: 75px; margin-right: 2px; background-color: teal; } .axis path, .axis line { fill: none; stroke:black; shape-rendering:crispEdges; } .axis text { font-size:11px; } p {width:300px;border:1px solid #ccc;border-radius: 3px;padding:5px;} </style> </head> <body> <p>Click on thie text to update the chart</p> <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script> <script src="https://d3js.org/d3.v3.js"></script> <script> //D3.js code let w = 800; let h = 200; let padding = 30; let svg = d3.select("body").append("svg").attr("width",w).attr("height",h);//把append()返回的新元素保存在了变量svg中 // let dataset = [ // [5,20],[480,90],[250,50],[100,33],[330,95],[410,12],[475,44],[25,67],[85,21],[220,88] // ]; let dataset = []; let numDataPoints = 50; let xRange = Math.random() * 1000; let yRange = Math.random() * 1000; for(let i = 0;i<numDataPoints;i++) { let newNumber1 = Math.floor(Math.random()* xRange); let newNumber2 = Math.floor(Math.random()* yRange); dataset.push([newNumber1,newNumber2]);//初始化随机数据集 } let xScale = d3.scale.linear() .domain([0,d3.max(dataset,function(d){return d[0];})]) .range([padding,w-padding*2]) .nice();//nice()告诉比例尺取得为range()设置的任何值域,把两端的值扩展到最接近的整数。如[0.2000011166,0.99999943]优化为[0.2,1] let yScale = d3.scale.linear() .domain([0,d3.max(dataset,function(d){return d[1];})]) .range([h-padding,padding]) .nice(); let rScale = d3.scale.linear() .domain([0,d3.max(dataset,function(d){return d[1];})]) .range([2,5]) .nice(); // 数轴 let xAxis = d3.svg.axis() .scale(xScale) .orient("bottom") .ticks(5); let yAxis = d3.svg.axis() .scale(yScale) .orient("left") .ticks(5); svg.selectAll("circle") .data(dataset) .enter() .append("circle") .attr("cx",function(d,i){ return xScale(d[0]); //返回缩放后的值 }) .attr("cy",function(d){ return yScale(d[1]); }) .attr("r",function(d){ return rScale(d[1]); }); //添加标签 // svg.selectAll("text") // .data(dataset) // .enter() // .append('text') // .text(function(d){ // return d[0]+ "," + d[1];//设置标签内容 // }) // .attr({ // fill : "black", // x : function(d) {return xScale(d[0])+10;},//将标签与散点位置一一对应 // y : function(d) {return yScale(d[1]);} // }) // .style("font-size", "11px"); //添加数轴 svg.append("g") .attr("class","x axis") .attr("transform","translate(0,"+(h-padding)+")") .call(xAxis); svg.append("g") .attr("class","y axis") .attr("transform","translate("+padding+",0)") .call(yAxis); d3.select("p") .on("click",function() {//selection.on()方法是添加事件监听器的简便方法,接受两个参数:事件类型和监听器(匿名函数) //p标签被单击时执行的任务 //新数据集 dataset = [ [5,20],[480,90],[250,50],[100,33],[330,95],[410,12],[475,44],[25,67],[85,21],[220,88] ]; //更新所有散点,注意到这里没有enter()和append() svg.selectAll("circle") .data(dataset) //重新绑定新数据 .transition() //过渡动画 .duration(1000) //过渡动画持续时间 1s .ease("linear") .each("start",function(){//过渡开始 d3.select(this) .attr("fill","magenta")//改变颜色 .attr("r",3)//改变半径 }) .attr("cx",function(d,i){ return xScale(d[0]); }) .attr("cy",function(d){ return yScale(d[1]); }) .each("end",function() { d3.select(this) .attr("fill","black") .attr("r",2); }); // .attr("r",function(d){ // return rScale(d[1]); // }); //更新比例尺值域 // yScale.domain([0,d3.max(dataset)]); //更新x轴 svg.select('.x.axis')//选择数轴 .transition()//初始化一个过渡 .duration(1000)//设定过渡的持续时间 .call(xAxis);//调用适当的数轴生成器 //更新y轴 svg.select('.y.axis') .transition() .duration(1000) .call(yAxis); }); </script> <script type="text/javascript"></script> </body>
(5)剪切路径
你可能注意到,在散点图更新中,x和y值较低的圆形会超出图表区域的边界,与轴线重叠在一起。
在SVG中,支持剪切路径(clipping:path),就是PS中的蒙版。剪切路径是一个SVG元素,可以包含可见的元素,并与这个可见元素一起构成可以应用到其他元素的剪切路径或蒙版。在把蒙版应用到某个元素时,只有落在该蒙版内的像素才会显示。
与g元素类似,clipPath也不可见,但它可以包含可见的元素。
使用剪切路劲的步骤如下:
- 定义clipPath并给它一个ID
- 在这个clipPath中放一个可见元素,如一个矩形
- 在需要使用蒙版的元素上添加一个对clipPath的引用;
//定义剪切路径 svg.append("clipPath") //创建clipPath元素 .attr("id", "chart-area") //指定Id .append("rect") //在clipPath中,创建并添加新的rect元素 .attr("x",padding) //设置rect的大小和位置 .attr("y",padding) .attr("width",w-padding*3) .attr("height",h-padding*2);
现在需要把这个蒙版应用到所有散点上,可以分别给每个散点添加一个对该clipPath的引用。
我们先把所有圆形放到一个组g中,然后给这个组添加引用。
svg.append("g")//对圆形编组 .attr("id","circles")//指定它的id为circles .attr("clip-path","url(#chart-area)") //添加对clipPath的引用 .selectAll("circle") .data(dataset) .enter() .append("circle") .attr("cx",function(d,i){ return xScale(d[0]); //返回缩放后的值 }) .attr("cy",function(d){ return yScale(d[1]); }) .attr("r",function(d){ return rScale(d[1]); });
如下图所示,我们建立了一个剪贴路径: