d3.js data-join(03)

前言:本篇主要是d3.js里面的datajoin

一、什么是Data-join?

本质上是将数据与图元进行绑定

例如:

  • 每个国家的人数绑定到矩形的长度
  • 疫情感染的人数比例绑定到圆的半径

为什么要使用data-join?

  • 使用data-join可以省去大量根据数据设置图元属性的代码量
  • 对于动态变化的数据提供统一的接口
  • 以数据为中心的可视化操作
    • 根据数据的每个属性自动调整绑定图元的属性
  • 不再需要手动添加、修改、删除图元
    • 会根据Data-join的绑定自动推断
  • 如果图元的数目不等于数据的条目,则根据数据条目的数量选定相应数量的图元

二、用函数设置图元属性

selection.attr('attrbuteName',value)

  • 支持直接通过值来设置属性
  • 支持通过函数来设置属性

selection.attr('attrbuteName',(d,i) = >{...})

  • d为绑定给图元的数据(即将到来)
  • i为图元的索引,是一个整数,如d3.selectAll(‘rect’)中的第几个矩形
  • 函数也可以仅使用d=>{…},但此时函数体无法使用索引
  • 即使不使用绑定的数据(如没有绑定数据),如需使用索引,仍需要完整的写出(d,i)=>{…}

三、数据与图元的绑定

data(dataArray)

(1)dataArray在保证是一个数组的前提下可以是任何形式

  • 例如:[0,2,4,5,56,999,45,67]
  • 例如:[{name:‘aaa’,age:18},{name:‘bbb’,age:15},{name:‘ccc’,age:90}]

(2)先考虑数据和图元数目相同的情况

  • dataArray是一个数组,其中的每‘条’数据会与一个图元绑定(反之亦然)
  • 绑定给每个图元的数据将对应.attr(,(d,i)=>{…})中的d
  • 默认的绑定按照双方的索引顺序
  • 不调用.data(…),则图元不会与任何数据绑定
  • 数据的更新只需要重新绑定另一个dataArray即可

四、d3.jd绑定数据的三个状态

d3.js data-join(03)_第1张图片

Updata:图元和数据条目相同

Enter:数据的条目多于图元甚至没有图元,常用于第一次绑定数据

Exit:数据的条目少于图元甚至没有数据,常用于结束可视化

4.1Updata

有图元有数据

const p = maingroup.selectAll('.dataourve').data(data).attr(...).attr(...)
  1. Updata作为实际可视化任务最常用的状态,经常被单独封装成一个函数
  2. UpdataSelection.merge(enterSelection).attr(…).attr(…)
    • 将两个Selection合并到一起操作
    • enterSelection在与updataSelectio n merge之前要至少已经调用了append(…)语句添加好图元。
  3. Updata经常与d3.js的动画一起使用

4.2Enter

有数据没图元,d3.jd会自动搞清楚哪些数据是新增的,根据新增的数据生成相应的图元。

生成图元的占位,占位内容需要编程者自行添加(append)

const p = maingroup.selectAll('.class').data(data).enter().append('').attr(...)

enter本质上生成指向父节点的指针,而append操作相当于在父节点后添加指针数量的图元并将其余多出的数据绑定。

4.3Exit

有图元没有数据,d3.js会自动“搞清楚”哪些图元是不绑定数据的

const p= maingroup.selectAll('.class').data(data).exit().remove()

4.4data-join的简洁形式

  • .data(...).join(...)

  • 默认enter和updata的执行形式相同

  • 默认exit是删除(remove)节点

  • 默认data-join形式简洁但不灵活

    • 必须需要设置enter数据的初始图元属性,updata会每次重新设置初始值,从而导致动画出现“奇怪”的效果

    • 仍支持“定制”:

    • .join(enter => enter.append('text').attr('fill','green').text(d=>d),
      updata =>(),exit=>())
      

五、让数据动起来

Updata经常与d3,js的动画一起使用

.transition().duration();
d3.selectAll('rect').data(data2,d=>d.name)
.transition().duration(3000).attr('width',d=>xScale(d.age))
//duration(...)中为毫秒
  • .transition(...)经过调用后,后续的链式调用会变成数值上的渐变,渐变的时间由.duration(...)决定
  • 插值的方式由.ease(…)设定

六、关于Key

.data(data,keyFunction)

  • keyFunction的返回值通常是一个字符串(string)

  • keyFunction的定义根据数据,比如keyFunction = d = >d.name

  • selection.data(data,d=>d.name);
    d3.selectAll('rect').data(data2,d=>d.name).attr('width',d=>xScale(d.age))
    

在绑定数据给图元时:

  • keyFunction为每条输入绑定的数据执行一次
  • keyFunction为每个包含数据的图元执行一次

如果图元之前没有绑定任何数据,则keyFunction会报错

  • 第一次绑定时根据索引即可
  • 实际的可视化任务,图元都是根据数据的“条”数动态添加(enter)、删除(exit)、只需要在添加时指定好DOM的ID即可

七、数据的读取

常见的是CSV数据。第一行为属性列表,后续每一行对应一条数据,CSV本质上是纯文本,区别于EXCEL的格式

d3.csv('path/to/data.csv').then(data=>{...})

  • .csv函数的返回值是一个js的‘Promise’对象(Promise对象用于执行异步操作)
  • .then(...)的参数为一个函数,参数为.csv(...)的返回值
  • d3.csv(...)会正常向服务器请求数据,在请求并处理好之后,将结果扔给.then(...)中的回调函数

八、一个栗子(画动态散点图)

数据要从服务器端获取,还涉及到跨域的问题。目前bug还没完全找出来


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>用d3的data-join散点图title>
    
    <script src='https://d3js.org/d3.v5.min.js'>script>
head>
<body style="text-align: center;">
    <svg width="1650" height="920" id="mainsvg" class="svgs" style="background-color: #ffffff;">svg>
    <script>
        const svg = d3.select('#mainsvg');
        const width = +svg.attr('width');
        const height = +svg.attr('height');
        const margin = {
      top:100,right:120,bottom:100,left:120};
        const innerWidth = width - margin.left - margin.right;
        const innerHeight = height - margin.top - margin.bottom;

        const xValue = d=>{
      return Math.log(d['确诊人数'] +1)};
        const yValue = d=>{
      return Math.log([d['新增确诊'] +1 ])};
        const rValue = d=>{
      return Math.log(d['感染率']*500)*0.8};
        const keyHint = '地区';
        let xScale,yScale;
        let maxX,maxY;
        let datas;
        let aduration = 1000;
        let metapop;

        const xAxisLabel = '累计确诊人数(对数)';
        const yAxisLabel = '新增人数(对数)';

        var color = {
      
            "武汉":"#ff1c12",
            "黄石": "#de5991",
            "十堰": "#759AA0",
            "荆州": "#E69D87",
            "宜昌": "#be3259",
            "襄阳": "#EA7E53",
            "鄂州": "#EEDD78",
            "荆门": "#9359b1",
            "孝感": "#47c0d4",
            "黄冈": "#F49F42",
            "咸宁": "#AA312C",
            "恩施州": "#B35E45",
            "随州": "#4B8E6F",
            "仙桃": "#ff8603",
            "天门": "#ffde1d",
            "潜江": "#1e9d95",
            "神农架": "#7289AB"
        }

        const renderinit = function(data,seq){
      
            xScale = d3.scaleLinear()
            .domain(d3.extent(data,xValue))
            .range([0,innerWeight])
            .nice();

            yScale = d3.scaleLinear()
            .domain(d3.extent(data,yValue).reverse())
            .range([0,innerHeight])
            .nice();

            maxX = xScale(d3.max(data,xValue));
            maxY = yScale(d3.max(data,yValue));
            //添加组
            const g = svg.append('g')
            .attr('transform',`translate(${
        margin.left},${
        margin.top})`)
            .attr('id','maingroup');

            const yAxis = d3.axisLeft(yScale)
            .tickSize(-innerWidth)
            .tickPadding(10);
            const xAxis = d3.axisBottom(xScale)
            .tickSize(-innerHeight)
            .tickPadding(10);

            let yAxisGroup = g.append('g').call(yAxis)
            .attr('id','yaxis');
            yAxisGroup.append('text')
            .attr('font-size','2em')
            .attr('transform',`rotate(-90)`)
            .attr('x',-innerHeight/2)
            .attr('y',-60)
            .attr('fill','#333333')
            .text(yAxisLabel)
            .attr('text-anchor','middle')
            yAxisGroup.selectAll('.domain').remove();

            let xAxisGroup = g.append('g').call(xAxis)
            .attr('transform', `translate(${
        0}, ${
        innerHeight})`)
            .attr('id', 'xaxis');
            xAxisGroup.append('text')
            .attr('font-size', '2em')
            .attr('y', 60)
            .attr('x', innerWidth / 2)
            .attr('fill', '#333333')
            .text(xAxisLabel);
            xAxisGroup.selectAll('.domain').remove();

            var legend_color = [
                "#ff1c12",
                "#de5991",
                "#759AA0",
                "#E69D87",
                "#be3259",
                "#EA7E53",
                "#EEDD78",
                "#9359b1",
                "#47c0d4",
                "#F49F42",
                "#AA312C",
                "#B35E45",
                "#4B8E6F",
                "#ff8603",
                "#ffde1d",
                "#1e9d95",
                "#7289AB"
            ]

            var legend_name = ["武汉市",
                "黄石市",
                "十堰市",
                "荆州市",
                "宜昌市",
                "襄阳市",
                "鄂州市",
                "荆门市",
                "孝感市",
                "黄冈市",
                "咸宁市",
                "恩施州",
                "随州市",
                "仙桃市",
                "天门市",
                "潜江市",
                "神农架",
            ];

            //下面是复制粘贴的
            var legend = d3.select('#maingroup').selectAll(".legend")
                .data(legend_name)
                .enter().append("g")
                .attr("class", "legend")
                .attr("transform", function(d, i) {
       return "translate(" + (innerWidth + 10) + "," + (i * 25 + 300) + ")"; });
			
            // draw legend colored rectangles
            legend.append("rect")
            .data(legend_name) 
            .attr("x", 0)
            .attr("y", 0)
            .attr("width", 30)
            .attr("height", 20)
            .style("fill", function (d,i) {
       
                return legend_color[i];});
			
            // draw legend text
            legend.append("text")
            .data(legend_name) 
            .attr('class', 'legend_text')
            .attr("x", 40)
            .attr("y", 9)
            .attr("dy", ".5em")
            .style("text-anchor", "start")
            .text(function (d,i) {
      return legend_name[i];}); 
        };

        const renderupdate = function(seq){
      
            const g = d3.select('#maingroup');
            time = seq[0]['日期'];
            g.selectAll('.date_text').remove();
            g.append("text")
            .data(['seq']) 
            .attr('class', 'date_text')
            .attr("x", innerWidth / 4 + 30)
            .attr("y", innerHeight / 10 - 20)
            .attr("dy", ".5em")
            .style("text-anchor", "end")
            .attr("fill", "#504f4f")
            .attr('font-size', '6em')
            .attr('font-weight', 'bold')
            .text(time);

            circleupdates = g.selectAll('circle').data(seq, d => d[keyHint]);
            circleenter = circleupdates.enter().append('circle')
            .attr('cy', (datum) => {
       return yScale(yValue(datum)) })
            .attr('cx', (datum) => {
       return xScale(xValue(datum)) }) // use xSacle to re-scale data space (domain) and return the rescaled population; 
            .attr('r', datum => rValue(datum))
            .attr('fill', function(d,i) {
       return color[d[keyHint]] })
            .attr('opacity', .8);
            circleupdates.merge(circleenter).transition().ease(d3.easeLinear).duration(aduration)
            .attr('cy', (datum) => {
       return yScale(yValue(datum)) })
            .attr('cx', (datum) => {
       return xScale(xValue(datum)) }) // use xSacle to re-scale data space (domain) and return the rescaled population; 
            .attr('r', datum => rValue(datum));

            textupdates = g.selectAll('.province_text').data(seq);
            textenter = textupdates.enter().append('text')
            .attr("class", "province_text")
            .attr("x", (datum) => {
       return xScale(xValue(datum)); })
            .attr("y", (datum) => {
       return yScale(yValue(datum)); })
            .attr("dy", "1em")
            .style("text-anchor", "middle")
            .attr("fill", "#333333")
            //.attr('opacity', 0)
            .text(function (d,i) {
       
                return d[keyHint];
            });

            textupdates.merge(textenter).transition().ease(d3.easeLinear).duration(aduration)
            .attr('x', (datum) => {
       
                return xScale(xValue(datum)); })
            .attr('y', (datum) => {
       return yScale(yValue(datum)); });
        };

        d3.csv('hubeipop.csv').then(data => {
      
            data.forEach(datum => {
      
                datum['人口(万人)'] = +(datum['人口(万人)']);
            })
            metapop = data;
        });

        d3.csv('hubei_day14.csv').then(function(data){
      
            data = data.filter(datum => {
      return datum[keyHint] !== '总计'});
            data.forEach(datum => {
      
                // pre-process the data; 
                datum['确诊人数'] = +(datum['确诊人数']);
                datum['治愈人数'] = +(datum['治愈人数']);
                datum['死亡人数'] = +(datum['死亡人数']);
                datum['新增确诊'] = +(datum['新增确诊']);
                if(datum['新增确诊'] < 0){
      
                    datum['新增确诊'] = 0;
                }
                datum['感染率'] = datum['确诊人数'] / 
                (metapop.find(x => x[keyHint] === datum[keyHint])['人口(万人)']);
            });

            // remove duplicated items; 
            alldates = Array.from(new Set(data.map( datum => datum['日期'])));

            // make sure dates are listed according to real time order; 
            alldates = alldates.sort(function(a,b){
      
                return new Date(b.date) - new Date(a.date);
            });
            dates = alldates;

            // re-arrange the data sequentially; 
            sequential = []; 
            alldates.forEach(datum => {
      
                sequential.push([]);
            });
            data.forEach(datum => {
      
                sequential[alldates.indexOf(datum['日期'])].push(datum);
            });

            renderinit(data, sequential[0]);

            // set the animation interval; 
            let c = 0; 
            intervalId = setInterval(function(){
      
                if(c >= alldates.length){
      
                    console.log('time to close this animation');
                    clearInterval(intervalId); 
                }else{
      
                    renderupdate(sequential[c]);
                    c = c + 1;
                }
            }, aduration); 
        });
    script>
body>
html>

你可能感兴趣的:(前端,d3.js,data-join,javascript)