d3.js 入门学习记录(三) 数据绑定简单实例

416 阅读7分钟

绑定数组数据

在熟悉 d3 的selection 和 数据绑定思想后,就可以编写简单的数据展示页面了。

代码如下:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .h-bar {
            min-height: 15px;
            min-width: 10px;
            background-color: steelblue;
            margin-bottom: 2px;
            font-size: 11px;
            color: #f0f8ff;
            text-align: right;
            padding-right: 2px;
        }
    </style>
</head>
<body>
<script src="../d3.js"></script>
<script>
    const data = [10, 15, 30, 50, 80, 65, 55, 30, 20, 10, 8]

    function render(data) {
        let bars = d3.select('body').selectAll('div.h-bar')
                .data(data)

        bars.enter()
                .append('div')
                    .attr('class', 'h-bar')
            .merge(bars)
                .style('width', function (d) {
                    return (d * 3) + 'px'
                })
                .text(function (d) {
                    return d
                })
        bars.exit()
                .remove()
    }

    setInterval(function () {
        data.shift()
        data.push(Math.round(Math.random() * 100))
        render(data);
    }, 1500)

    render(data)

</script>
</body>
</html>

效果如下:

让我们来一步步解析这段简单的示例:

我们先定义了一个data数组,里面存放了我们要准备展示的数据,render函数包含了把data渲染到页面上的操作。我们通过定时器,不断删除data的第一个元素,并在末尾添加一个随机的数据,然后重新渲染。

在 render 函数中,我们首先是通过选择器获取到 body 下所有类名为 h-bar 的 div 元素,但是实际上页面中并没有 div 元素,所以这是个空集,按照我们之前的介绍,在这种情况下使用 data() 方法绑定数据,就相当是创建了许多个空的占位符,之后我们调用 bars.enter().append('div').attr('class', 'h-bar') 为 enter 选择集添加了 dom 元素,所以 11个 div 被加入了 body 里面。然后我们使用 merge() 方法,将对 enter 选择集和 update 选择集的操作集中起来,避免代码重复。 之后添加对 exit 选择集的 dom 删除操作。

在render 中,我们要理解为什么使用 merge() 方法:在第一次的渲染中,页面中没有任何匹配的div 元素, 我们的 update 和 exit 选择集是空的,只有enter 选择集,所以在第一次的渲染中我们要对 enter 选择集进行数据展示到dom的操作。同样的,在第一次render 后 body 下已经存在了 11 个 div 元素,所以在第二次及以后的渲染中,我们没有改变data的数据个数,所以第一次之后的渲染中,只有 update 选择集,enter 和 exit 选择集都是没有的,所以我们对数据展示到 dom 上的操作要对 update选择集操作。那我们既要对 enter 选择集操作,又要对update选择集操作,自然是使用 merge() 合并到一起操作。

其中 bars.exit().remove() 是没有起到作用的,只不过是为了展示一个完整的过程。如果我们在第二次渲染中将 data 的数据项变为 10 项,这段代码就会起作用,帮我么移除多余的那个没有匹配到数据项的 dom 元素。

绑定复杂数据

在上面的例子中,我们只对简单的数字数据进行了操作,在对复杂的对象进行操作时,其实也是同样的操作。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="../style.css">
    <title>Document</title>
</head>
<body>
<script src="../d3.js"></script>
<script>
    const data = [
        {width: 10, color: 23},
        {width: 15, color: 33},
        {width: 30, color: 40},
        {width: 50, color: 60},
        {width: 80, color: 22},
        {width: 65, color: 10},
        {width: 55, color: 5},
        {width: 30, color: 30},
        {width: 20, color: 60},
        {width: 10, color: 90},
        {width: 8, color: 10},
    ]

    let colorScale = d3.scaleLinear().domain([0, 100]).range(['#add8e6', 'blue'])

    function render(data) {
        let bars = d3.select('body').selectAll('div.h-bar').data(data)

        bars.enter()
                .append('div')
                .attr('class', 'h-bar')
            .merge(bars)
                .style('width', function (d) {
                    return (d.width * 5) + 'px'
                })
                .style('background-color', function (d) {
                    return colorScale(d.color)
                })
                .text(function (d) {
                    return d.width
                })

        bars.exit().remove()
    }

    function randomValue() {
        return Math.round(Math.random() * 100)
    }

    setInterval(function () {
        data.shift()
        data.push({width: randomValue(), color: randomValue()})
        render(data)
    }, 1500)

    render(data)
</script>
</body>
</html>

效果如下:

这里使用到了scale,我们马上会在后面讲解,我们现在只需要知道colorScale()函数会返回一个对应的颜色即可。

对于对象的数据项,我们也只是 d.color 这么简单的一步就解决了。

绑定函数数据

data() 方法除了绑定数组数据项外,还可以绑定返回一个数组的函数。函数的参数为 d(数据) i(索引) nodes(父节点),函数内部 this 指向当前分组的父元素。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .v-bar {
            min-width: 30px;
            background-color: #4682b4;
            margin-right: 2px;
            font-size: 10px;
            color: #f0f8ff;
            text-align: center;
            display: inline-block;
        }
    </style>
</head>
<body>
<div id="container"></div>
<script src="../d3.js"></script>
<script>
    const data = []
    const datum = function (x) {
        return 15 + x * x
    }

    let newData = function () {
        data.push(datum)
        return data
    }

    function render() {
        let divs = d3.select('#container').selectAll('div').data(newData)
        divs.enter()
            .append('div').append('span')
        divs.attr('class', 'v-bar')
            .style('height', function (d, i) {
                return d(i) + 'px'
            })
            .select('span')
                .text(function (d, i) {
                    return d(i)
                })
        divs.exit().remove()
    }

    setInterval(function () {
        render()
    }, 1000)
    render()

</script>
</body>
</html>

将函数绑定为数据项其实最实用的还是为子选择集绑定子数据,如:

const matrix = [
  [11975,  5871, 8916, 2868],
  [ 1951, 10048, 2060, 6171],
  [ 8010, 16145, 8090, 8045],
  [ 1013,   990,  940, 6907]
];

const tr = d3.select("body").append("table").selectAll("tr")
  .data(matrix)
  .enter()
    .append("tr");

const td = tr.selectAll("td")
  .data(function(d) { return d; })
  .enter()
    .append("td")
      .text(function(d) { return d; });

数据的过滤

我们可以使用 selection.filter() 方法,来实现对筛选的元素进行操作。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .h-bar {
            min-height: 15px;
            min-width: 10px;
            background-color: steelblue;
            margin-bottom: 2px;
            font-size: 11px;
            color: #f0f8ff;
            text-align: right;
            padding-right: 2px;
        }

        .selected {
            background-color: #f08080;
        }
    </style>
</head>
<body>
<div class="control-group">
    <button onclick="select('Retail')">
        Retail
    </button>
    <button onclick="select('Gas')">
        Gas
    </button>
    <button onclick="select('Dining')">
        Dining
    </button>
    <button onclick="select()">
        Clear
    </button>
</div>

<script src="../d3.js"></script>
<script>
    const data = [
        {expense: 10, category: 'Retail'},
        {expense: 15, category: 'Gas'},
        {expense: 30, category: 'Retail'},
        {expense: 50, category: 'Dining'},
        {expense: 80, category: 'Gas'},
        {expense: 65, category: 'Retail'},
        {expense: 55, category: 'Gas'},
        {expense: 30, category: 'Dining'},
        {expense: 20, category: 'Retail'},
        {expense: 10, category: 'Dining'},
        {expense: 8, category: 'Gas'},
    ]

    function render(data, category) {
        let bars = d3.select('body').selectAll('div.h-bar')
            .data(data)

        bars.enter()
            .append('div')
                .attr('class', 'h-bar')
                .style('width', function (d) {
                    return (d.expense * 5) + 'px'
                })
            .append('span')
                .text(function (d) {
                    return d.category
                })

        d3.selectAll('div.selected').classed('selected', false)

        bars.filter(function (d, i) {
                return d.category === category
            })
            .classed('selected', true)
    }

    render(data)

    function select(category) {
        render(data, category)
    }
</script>
</body>
</html>

效果如下:

数据的排序

同样的使用 selector.sort() 可以达到常用的排序效果。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .h-bar {
            min-height: 15px;
            min-width: 10px;
            background-color: steelblue;
            margin-bottom: 2px;
            font-size: 11px;
            color: #f0f8ff;
            text-align: right;
            padding-right: 2px;
        }
    </style>
</head>
<body>
<div class="control-group">
    <button onclick="sort(compareByExpense)">
        Sort by Expense
    </button>
    <button onclick="sort(compareByCategory)">
        Sort by Category
    </button>
    <button onclick="sort()">
        Reset
    </button>
</div>
<script src="../d3.js"></script>
<script>
    let data = [
        {expense: 10, category: 'Retail'},
        {expense: 15, category: 'Gas'},
        {expense: 30, category: 'Retail'},
        {expense: 50, category: 'Dining'},
        {expense: 80, category: 'Gas'},
        {expense: 65, category: 'Retail'},
        {expense: 55, category: 'Gas'},
        {expense: 30, category: 'Dining'},
        {expense: 20, category: 'Retail'},
        {expense: 10, category: 'Dining'},
        {expense: 8, category: 'Gas'},
    ]

    function render(data, comparator) {
        let bars = d3.select('body').selectAll('div.h-bar')
            .data(data)

        bars.enter()
            .append('div')
                .attr('class', 'h-bar')
            .append('span')

        d3.selectAll('div.h-bar')
            .style('width', function (d) {
                return (d.expense * 5) + 'px'
            })
            .select('span')
                .text(function (d) {
                    return d.category
                })

        if (comparator) {
            bars.sort(comparator)
        }
    }

    const compareByExpense = function (a, b) {
        return a.expense < b.expense ? -1 : 1
    }

    const compareByCategory = function (a, b) {
        return a.category < b.category ? -1 : 1
    }

    render(data)

    function sort(comparator) {
        render(data, comparator)
    }
</script>
</body>
</html>

效果如下: