【d3.js入门】基本柱状图动画

758 阅读6分钟

前面写了一篇 【d3.js入门】基本柱状图,这次我们在这个柱状图基础上添加一些动画效果。

bar-ani.gif

好,废话多说,我们再画一遍柱状图。

绘制柱状图

D3.js 是一个 JavaScript 库,用于创建动态、交互式的数据可视化,它能够将数据与一系列 DOM 元素绑定,进而让开发者可以使用数据驱动的方式来操作 DOM 元素,从而实现各种可视化效果,包括柱状图、折线图、饼图等等。

使用 D3.js 来制作一个简单的柱状图,以下是代码和讲解:

// 创建 div 容器,并将其添加到 body 中
var dom = document.createElement('div');
document.body.appendChild(dom);

// 数据集,每个对象都有一个 name 和 value 属性
var dataset = [
  { name: "1", value: 30 },
  { name: "2", value: 20 },
  { name: "3", value: 50 },
  { name: "4", value: 20 },
  { name: "5", value: 100 },
  { name: "6", value: 30 },
  { name: "7", value: 60 }
];

// 设定柱状图的 padding、宽度和高度
var padding = 30;
var svgWidth = 600;
var svgHeight = 400;

// 创建 x 轴的比例尺,使用 scaleBand() 函数
var xScale = d3.scaleBand()
  .domain(d3.range(dataset.length)) // 指定输入域,使用 d3.range() 函数生成长度为 dataset.length 的数组
  .range([padding, svgWidth - padding]) // 指定输出域,从 padding 到 svgWidth - padding
  .padding(0.3); // 设置柱状之间的间隔,默认为 0.1

// 创建 y 轴的比例尺,使用 scaleLinear() 函数
var yScale = d3.scaleLinear()
  .domain([0, d3.max(dataset, function(d) { return d.value; })]) // 指定输入域,使用 d3.max() 函数获取 dataset 中的最大值
  .range([svgHeight - padding, padding]); // 指定输出域,从 svgHeight - padding 到 padding

// 创建 svg 元素,并设定其宽度和高度
var svg = d3.select(dom)
  .append("svg")
  .attr("width", svgWidth)
  .attr("height", svgHeight)
  .style('border', '1px solid #999999');

// 计算每个柱状的宽度
var barWidth = xScale.bandwidth();

// 创建柱状图的矩形元素
var bars = svg.selectAll(".bar")
  .data(dataset) // 绑定数据
  .join('rect') // 使用 join() 函数创建矩形元素
  .attr("class", "bar") // 添加类名
  .attr("fill", "#69b3a2") // 柱状图的颜色
  .attr("x", function(d, i) { // 指定 x 坐标,使用 xScale 函数
    return xScale(i);
  })
  .attr("y", function(d) { // 指定 y 坐标,使用 yScale 函数
    return yScale(d.value);
  })
  .attr("width", barWidth) // 指定柱状的宽度
  .attr("height", function(d) { // 指定柱状的高度,使用 yScale 函数
    return svgHeight - yScale(d.value) - padding;
  });

// 创建 x 轴,使用 axisBottom() 函数
var xAxis = d3.axisBottom(xScale)
  .tickFormat(function(d, i) { // 指定刻度标签的格式,使用 dataset 中的 name 属性
    return dataset[i].name;
  });

// 创建 y 轴,使用 axisLeft() 函数
var yAxis = d3.axisLeft(yScale);

// 将 x 轴添加到 svg 中,并设定其位置
svg.append("g")
  .attr("class", "x-axis")
  .attr("transform", "translate(0," + (svgHeight - padding) + ")")
  .call(xAxis);

// 将 y 轴添加到 svg 中,并设定其位置
svg.append("g")
  .attr("class", "y-axis")
  .attr("transform", "translate(" + padding + ",0)")
  .call(yAxis);

上述代码创建了一个简单的柱状图,其中包含了以下几个关键的知识点:

  1. D3.js 的比例尺(scale):比例尺用于将输入域(domain)中的数据映射到输出域(range)中的一组值。在本例中,使用了 scaleBand() 和 scaleLinear() 函数分别创建了 x 轴和 y 轴的比例尺。

  2. D3.js 的选择集(selection):选择集是 D3.js 中最基本的概念之一,它用于选择 DOM 元素并进行操作。在本例中,使用了 select() 和 selectAll() 函数来选择 DOM 元素。

  3. D3.js 的数据绑定(data binding):数据绑定是 D3.js 中最重要的概念之一,它用于将数据集与 DOM 元素绑定,并根据数据集的内容来动态地创建、更新、删除 DOM 元素。在本例中,使用了 data() 函数将数据集与柱状图的矩形元素绑定,并使用 join() 函数来创建矩形元素。

  4. D3.js 的坐标轴(axis):坐标轴用于显示比例尺的刻度标签和线条。在本例中,使用了 axisBottom() 和 axisLeft() 函数来创建 x 轴和 y 轴,并使用 call() 函数来将坐标轴添加到 svg 元素中。

添加动画

我们添加几个按钮用来更新数据和对数据排序,由于D3属于数据驱动的,所以我们只需要对数据进行操作即可。

首先,我们使用 d3.select() 方法选择一个 div 元素,并将其作为 SVG 容器。然后,我们定义了一些数据集,用于生成图表。我们创建了两个比例尺(x 和 y),它们将数据值映射到坐标轴上。接下来,我们创建了一组矩形元素,它们代表数据集中的每个数据点。我们使用 selectAll()data() 方法将数据集绑定到这些矩形元素上,并使用 enter() 方法将新元素添加到 DOM 中。最后,我们使用 append() 方法向每个元素添加一个矩形,并为每个矩形设置位置、大小和颜色。

然后,我们添加了三个按钮。第一个按钮用于更新数据集中的值。当用户单击“更新数据”按钮时,我们使用 Math.random() 方法生成一个新的随机值,并将其分配给每个数据点。然后,我们使用 transition() 方法将更改应用到每个矩形元素上,并使用 duration() 方法指定动画持续时间。

第二个和第三个按钮用于对数据进行排序。当用户单击“正向排序”按钮时,我们使用 sort() 方法将数据集按升序排序,并将新的 x 轴域应用于比例尺。然后,我们使用 transition() 方法将更改应用到每个矩形元素上,并使用 duration() 方法指定动画持续时间。当用户单击“反向排序”按钮时,我们使用 sort() 方法将数据集按降序排序,并使用相同的过渡方法更新图表。

最后是添加数据和删除数据。

代码如下:

// 添加按钮 更新数据 
var butdiv = document.createElement('div');
dom.appendChild(butdiv);
var but1 = document.createElement('button');
but1.innerHTML = '更新数据';
butdiv.appendChild(but1);
but1.onclick = function () {
    dataset.forEach(function (d) {
        d.value = Math.floor(Math.random() * 90) + 10;
    });
    svg.selectAll(".bar")
        .data(dataset)
        .join('rect')
        .transition()
        .duration(1000)
        .attr("y", function (d) {
            return yScale(d.value);
        })
        .attr("height", function (d) {
            return svgHeight - yScale(d.value) - padding;
        });
}
// 添加按钮 正向排序
var but2 = document.createElement('button');
but2.innerHTML = '正向排序';
butdiv.appendChild(but2);
but2.onclick = function () {
    dataset.sort(function (a, b) {
        return a.value - b.value;
    });
    xScale.domain(d3.range(dataset.length));
    svg.selectAll(".bar")
        .data(dataset)
        .join('rect')
        .transition()
        .duration(1000)
        .attr("x", function (d, i) {
            return xScale(i);
        })
        .attr("y", function (d) {
            return yScale(d.value);
        })
        .attr("height", function (d) {
            return svgHeight - yScale(d.value) - padding;
        });
}
// 添加按钮 反向排序
var but3 = document.createElement('button');
but3.innerHTML = '反向排序';
butdiv.appendChild(but3);
but3.onclick = function () {
    dataset.sort(function (a, b) {
        return b.value - a.value;
    });
    xScale.domain(d3.range(dataset.length));
    svg.selectAll(".bar")
        .data(dataset)
        .join('rect')
        .transition()
        .duration(1000)
        .attr("x", function (d, i) {
            return xScale(i);
        })
        .attr("y", function (d) {
            return yScale(d.value);
        })
        .attr("height", function (d) {
            return svgHeight - yScale(d.value) - padding;
        });
}

// 添加按钮 添加数据
var but4 = document.createElement('button');
but4.innerHTML = '添加数据';
butdiv.appendChild(but4);
but4.onclick = function () {
    var newNum = Math.floor(Math.random() * 90) + 10;
    dataset.push({ name: dataset.length + 1, value: newNum });
    xScale.domain(d3.range(dataset.length));
    // 更新x轴
    svg.select(".x-axis")
        .transition()
        .duration(1000)
        .call(xAxis);
    // 更新柱子
    barWidth = xScale.bandwidth();
    svg.selectAll(".bar")
        .data(dataset)
        .join('rect')
        .attr("class", "bar")
        .attr("fill", "#69b3a2")
        .attr("x", function (d, i) {
            return xScale(i);
        })
        .attr("width", barWidth)
        .transition()
        .duration(1000)
        .attr("y", function (d) {
            return yScale(d.value);
        })
        .attr("height", function (d) {
            return svgHeight - yScale(d.value) - padding;
        });

}

// 添加按钮 删除数据
var but5 = document.createElement('button');
but5.innerHTML = '删除数据';
butdiv.appendChild(but5);
but5.onclick = function () {
    if(dataset.length<=7) return;
    // 删除最后一项
    dataset.pop();
    xScale.domain(d3.range(dataset.length));
    // 更新x轴
    svg.select(".x-axis")
        .transition()
        .duration(1000)
        .call(xAxis);
    // 更新柱子
    barWidth = xScale.bandwidth();
    svg.selectAll(".bar")
        .data(dataset)
        .join('rect')
        .attr("class", "bar")
        .attr("fill", "#69b3a2")
        .attr("x", function (d, i) {
            return xScale(i);
        })
        .attr("width", barWidth)
        .transition()
        .duration(1000)
        .attr("y", function (d) {
            return yScale(d.value);
        })
        .attr("height", function (d) {
            return svgHeight - yScale(d.value) - padding;
        });
}

[在线地址:scqilin.github.io/d3js/basic-…]