【D3使用教程】(5) 动态更新与过渡动画

数据总是在变化的,那么我们要如何将变化的数据反映到图表上呢?
在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.length3,那么就是[0,1,2]

(2)自动分档

与线性比例尺使用的连续范围值不同,序数比例尺使用的是离散范围值,即输出值是事先确定好的,可以是数值,也可以不是。
在映射范围时,可以使用range(),也可以使用rangeBands()。后者接收一个最小值和一个最大值,然后根据输入值域的长度自动将其切分成相等的块域或“档”,如:
.rangeBands([0.w])//计算从0到w可以均分为几档,然后把比例尺的范围设定为这些“档”,例如有3档,那么w/3为每一档的“宽度”。还可以给rangeBands()传入第二个参数,指定档之间的间距。

rangeRoundBands()会对输出的值舍入为最接近的整数。如3.1415变成3。整数值有助于将视觉元素与像素网格对齐。

(3)更新

到目前为止,我们的代码还是随着页面的加载执行。对于更新数据来说,可以在开始的绘制代码一执行完毕就更新,但这样更新太快。为了能看到更新的变化,需要把更新的代码与其他代码分开。因此,需要在页面加载之后添加一个“触发器”,用以触发数据和图表的更新。例如,使用鼠标点击事件。

  • 通过事件监听实现交互

首先在body中添加一个p标签,用于点击事件更新图表:

Click on thie text to update the chart

接着在D3代码最后,添加D3的事件监听。

d3.select("p")
  .on("click",function() {//selection.on()方法是添加事件监听器的简便方法,接受两个参数:事件类型和监听器(匿名函数)
    //p标签被单击时执行的任务
    alert("Hey!");
  });

接下来,我们需要改变数据,或者说更新数据。为此,需要:

  1. 重新绑定新数据与已有元素;
  2. 选择相应的图形,如散点、矩形,再调用一次data()方法; 例如这里,我们选择散点(圆形)为例:
  3. 最后更新视觉元素的属性,以反映更新后的数据值
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)完成代码


<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>

(5)剪切路径

你可能注意到,在散点图更新中,x和y值较低的圆形会超出图表区域的边界,与轴线重叠在一起。

在SVG中,支持剪切路径(clipping:path),就是PS中的蒙版。剪切路径是一个SVG元素,可以包含可见的元素,并与这个可见元素一起构成可以应用到其他元素的剪切路径或蒙版。在把蒙版应用到某个元素时,只有落在该蒙版内的像素才会显示。

与g元素类似,clipPath也不可见,但它可以包含可见的元素。

使用剪切路劲的步骤如下:

  1. 定义clipPath并给它一个ID
  2. 在这个clipPath中放一个可见元素,如一个矩形
  3. 在需要使用蒙版的元素上添加一个对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]);
    });

如下图所示,我们建立了一个剪贴路径:
【D3使用教程】(5) 动态更新与过渡动画_第1张图片

你可能感兴趣的:(D3,数据可视化)