数据总是在变化的,那么我们要如何将变化的数据反映到图表上呢?
在D3中,这些变化通过更新进行处理。而过渡通过使用动画用于处理视觉上的展示。
首先,我们定义一个序数比例尺:
let xScale = d3.scale.ordinal()//定义一个序数比例尺,用于处理序数
注:序数是一些有固定顺序的一些类别,如:
然后,为比例尺设定输入值的值域。在线性比例尺中,用包含两个值的数组来设置值域,如[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]
与线性比例尺使用的连续范围值不同,序数比例尺使用的是离散范围值,即输出值是事先确定好的,可以是数值,也可以不是。
在映射范围时,可以使用range()
,也可以使用rangeBands()
。后者接收一个最小值和一个最大值,然后根据输入值域的长度自动将其切分成相等的块域或“档”,如:
.rangeBands([0.w])
//计算从0到w可以均分为几档,然后把比例尺的范围设定为这些“档”,例如有3档,那么w/3为每一档的“宽度”。还可以给rangeBands()传入第二个参数,指定档之间的间距。
rangeRoundBands()
会对输出的值舍入为最接近的整数。如3.1415变成3。整数值有助于将视觉元素与像素网格对齐。
到目前为止,我们的代码还是随着页面的加载执行。对于更新数据来说,可以在开始的绘制代码一执行完毕就更新,但这样更新太快。为了能看到更新的变化,需要把更新的代码与其他代码分开。因此,需要在页面加载之后添加一个“触发器”,用以触发数据和图表的更新。例如,使用鼠标点击事件。
首先在body中添加一个p标签,用于点击事件更新图表:
Click on thie text to update the chart
接着在D3代码最后,添加D3的事件监听。
d3.select("p")
.on("click",function() {//selection.on()方法是添加事件监听器的简便方法,接受两个参数:事件类型和监听器(匿名函数)
//p标签被单击时执行的任务
alert("Hey!");
});
接下来,我们需要改变数据,或者说更新数据。为此,需要:
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(){})
可以设置。
<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>Documenttitle>
<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 chartp>
<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;ilet 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>
你可能注意到,在散点图更新中,x和y值较低的圆形会超出图表区域的边界,与轴线重叠在一起。
在SVG中,支持剪切路径(clipping:path),就是PS中的蒙版。剪切路径是一个SVG元素,可以包含可见的元素,并与这个可见元素一起构成可以应用到其他元素的剪切路径或蒙版。在把蒙版应用到某个元素时,只有落在该蒙版内的像素才会显示。
与g元素类似,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]);
});