D3.js学习笔记

1,177 阅读9分钟

image.png

本文是自己为了实现一个类似甘特图的效果学习的D3笔记,参考了 掘金 @混沌传奇 的教程juejin.cn/post/684490… ,如侵删。对于其中的每个示例,都用 D3.js 5.12.0的版本实现了一遍,并给出链接,还是有点小坑的。

什么是D3.js


D3.js的全称是Data-Driven Documents,其实就是一个数据驱动的文档的js库,简而言之就是一个数据可视化的库。那什么是数据可视化呢?举个例子:


给出一组数据 [10,80,40,100,30,20,50]

如果我们想要看出这组数据的大小关系,单看数据显然不够直观。那么我们可以将它转换为一种简单易懂的图表的形式(如下图)这样我们就可以更加直观的获取数据所传递给我们的信息。这个过程就叫做数据可视化。


在我们平时的项目开发中经常会遇到这种情况:

后端返回给我们一组数据,前端人员需要把数据以一种很舒服、很直观的方式给别人展现出来,最好的选择就是通过图表,图表可以很直观的把庞大的数据以一种合适的方式展现给我们。但是如果通过js,css去自己写出这些图表显然是很麻烦的。所以就有D3.js。


第一个程序:Hello World

<html> 
  <head> 
        <meta charset="utf-8"> 
        <title>HelloWorld</title> 
  </head> 
    <body> 
        <p>Hello World 1</p>
        <p>Hello World 2</p>
    </body> 
</html>

用 JavaScript 来更改 HelloWorld

<script>
        var paragraphs = document.getElementsByTagName("p");
        for (var i = 0; i < paragraphs.length; i++) {
          var paragraph = paragraphs.item(i);
          paragraph.innerHTML = "I like dog.";
        }          
        </script> 

如何用 D3 来更改 HelloWorld呢?

d3.select("body").selectAll("p").text("I like dog.");

D3 能够连续不断地调用函数,类似于jQuery的链式语法

D3的实现: 链接


做一个简单的图表

要绘图,首要需要的是一块绘图的“画布”。

HTML 5 提供两种强有力的“画布”:SVGCanvas


SVG 有如下特点:

  • SVG 绘制的是矢量图,因此对图像进行放大不会失真。
  • 基于 XML,可以为每个元素添加 JavaScript 事件处理器。
  • 每个图形均视为对象,更改对象的属性,图形也会改变。
  • 不适合游戏应用。

Canvas 有如下特点:

  • 绘制的是位图,图像放大后会失真。
  • 不支持事件处理器。
  • 能够以 .png 或 .jpg 格式保存图像
  • 适合游戏应用。


D3:

Echarts:

太底层,学习成本大

兼容到IE9以上以及所有的主流浏览器

D3通过svg来绘制图形

可以自定义事件

封装好的方法直接调用

兼容到ie6以及以上的所有主流浏览器

echarts通过canvas来绘制图形

封装好的,直接用,不能修改

Svg:

Canvas:

不依赖分辨率

基于xml绘制图形,可以操作dom

支持事件处理器

复杂度高,会减慢页面的渲染速度

依赖分辨率

基于js绘制图形

不支持事件处理器

能以png或者jpg的格式保存图片


添加画布

var width = 300;  //画布的宽度
var height = 300;   //画布的高度
var svg = d3.select("body")     //选择文档中的body元素
    .append("svg")          //添加一个svg元素
    .attr("width", width)       //设定宽度
    .attr("height", height);    //设定高度

有了画布,接下来就可以在画布上作画了。


绘制矩形

svg中矩形用<rect>表示

<svg>
  <rect></rect>
  <rect></rect>
</svg>

rect具有很多属性,常用如下:

  • x:矩形左上角的 x 坐标
  • y:矩形左上角的 y 坐标
  • width:矩形的宽度
  • height:矩形的高度

要注意,在 SVG 中,x 轴的正方向是水平向右,y 轴的正方向是垂直向下的。



现在给出一组数据,要对此进行可视化。数据如下:

var dataset = [ 250 , 210 , 170 , 130 , 90 ];  //数据(表示矩形的宽度)

为简单起见,我们直接用数值的大小来表示矩形的像素宽度(实际上并没有人这样用)添加入下代码:


var rectHeight = 25;   //每个矩形所占的像素高度(包括空白)

svg.selectAll("rect")                //选择svg内所有的矩形
    .data(dataset)                   //绑定数组
    .enter()                         //指定选择集的enter部分
    .append("rect")                  //添加足够数量的矩形元素
    .attr("x",20)
    .attr("y",function(d,i){
         return i * rectHeight;
    })
    .attr("width",function(d){
         return d;
    })
    .attr("height",rectHeight-2)
    .attr("fill","steelblue");

这样就成功绘制出了矩形,链接

image.png

比例尺

前面有说到用数据的值来表示图像的宽度并不是一个很好的做法。

上面的数组:

var dataset = [ 250 , 210 , 170 , 130 , 90 ];

绘图时,直接使用 250 给矩形的宽度赋值,即矩形的宽度就是 250 个像素。

此方式非常具有局限性,如果数值过大或过小,例如:

var dataset_1 = [ 2.5 , 2.1 , 1.7 , 1.3 , 0.9 ];
var dataset_2 = [ 2500, 2100, 1700, 1300, 900 ];

对以上两个数组,绝不可能用 2.5 个像素来代表矩形的宽度,那样根本看不见;也不可能用 2500 个像素来代表矩形的宽度,因为画布没有那么长。

于是,我们需要一种计算关系,能够:

将某一区域的值映射到另一区域,其大小关系不变。

这就是比例尺(Scale)。


D3提供了多种比例尺,下面介绍最常用的两种:

线性比例尺

线性比例尺,能将一个连续的区间,映射到另一区间。要解决柱形图宽度的问题,就需要线性比例尺。

假设有以下数组:

var dataset = [1.2, 2.3, 0.9, 1.5, 3.3];

现有要求如下:

将 dataset 中最小的值,映射成 0;将最大的值,映射成 300。

代码如下:

var min = d3.min(dataset);
var max = d3.max(dataset);
var linear = d3.scaleLinear()
        .domain([min, max])
        .range([0, 300]);
linear(0.9);    //返回 0
linear(2.3);    //返回 175
linear(3.3);    //返回 300

其中,d3.scale.linear() 返回一个线性比例尺。domain() 和 range() 分别设定比例尺的定义域和值域。在这里还用到了两个函数,它们经常与比例尺一起出现:

  • d3.max()
  • d3.min()

这两个函数能够求数组的最大值和最小值,是 D3 提供的。按照以上代码,

比例尺的定义域 domain 为:[0.9, 3.3]

比例尺的值域 range 为:[0, 300]

因此,当输入 0.9 时,返回 0;当输入 3.3 时,返回 300。当输入 2.3 时呢?返回 175,这是按照线性函数的规则计算的。

有一点请大家记住:

d3.scale.linear() 的返回值,是可以当做函数来使用的。因此,才有这样的用法:linear(0.9)。


序数比例尺

有时候,定义域和值域不一定是连续的。例如,有两个数组:

var index = [0, 1, 2, 3, 4];
var color = ["red", "blue", "green", "yellow", "black"];

我们希望 0 对应颜色 red,1 对应 blue,依次类推。

但是,这些值都是离散的,线性比例尺不适合,需要用到序数比例尺。

var ordinal = d3.scaleOrdinal()
        .domain(index)
        .range(color);
ordinal(0); //返回 red
ordinal(2); //返回 green
ordinal(4); //返回 black

用法与线性比例尺是类似的。


链接


用比例尺绘图

定义一个线性比例尺。

var dataset = [ 2.5 , 2.1 , 1.7 , 1.3 , 0.9 ];
var linear = d3.scaleLinear()
        .domain([0, d3.max(dataset)])
        .range([0, 250]);

如之前添加矩形,设置宽度的时候使用比例尺

var rectHeight = 25;   //每个矩形所占的像素高度(包括空白)
svg.selectAll("rect")
    .data(dataset)
    .enter()
    .append("rect")
    .attr("x",20)
    .attr("y",function(d,i){
         return i * rectHeight;
    })
    .attr("width",function(d){
         return linear(d);   //在这里用比例尺
    })
    .attr("height",rectHeight-2)
    .attr("fill","steelblue");

codepen.io/Ricardo1601…


坐标轴

svg中并没有提供坐标轴图像,需要手工绘制拼接。D3封装了这部分操作,提供了坐标轴组件 d3.svg.axis()

创建一个坐标轴


var axis = d3.axisBottom(linear).ticks(5);


在svg中添加这个坐标轴

svg.append("g")
   .call(axis);

这样就可以在图中显示,但是不够美观,添加CSS美化


<style>
    .axis path, .axis line{
      fill: none;
      stroke: black;
      shap-rendering: orispEdges;
    }
    .axis text {
      font-family: sans-serif;
      font-size: 11px;
    }
  </style>


svg.append("g")
.attr("class","axis")
.attr("transform","translate(20,130)")
.call(axis);

image.png

codepen.io/Ricardo1601…


完整的柱形图

codepen.io/Ricardo1601…

var width = 400;
var height = 400;
var rectPadding = 4;
//在 body 里添加一个 SVG 画布
var svg = d3
  .select("body")
  .append("svg")
  .attr("width", width)
  .attr("height", height);
console.log(svg);
var dataset = [10, 20, 30, 40, 33, 24, 12, 5];

var padding = { left: 30, right: 30, top: 20, bottom: 20 };

var xScale = d3
  .scaleBand()
  .domain(d3.range(dataset.length))
  .rangeRound([0, width - padding.left - padding.right]);

var xAxis = d3.axisBottom(xScale);

var yScale = d3
  .scaleLinear()
  .domain([0, d3.max(dataset)])
  .range([height - padding.top - padding.bottom, 0]);

var yAxis = d3.axisLeft(yScale);

var rects = svg
  .selectAll(".MyRect")
  .data(dataset)
  .enter()
  .append("rect")
  .attr("class", "MyRect")
  .attr("transform", "translate(" + padding.left + "," + padding.top + ")")
  .attr("x", function(d, i) {
    return xScale(i) + rectPadding / 2;
  })
  .attr("y", function(d) {
    return yScale(d);
  })
  .attr("width", function() {
    return xScale.step() - rectPadding;
  })
  .attr("height", function(d) {
    return height - padding.top - padding.bottom - yScale(d);
  });

var texts = svg
  .selectAll(".MyText")
  .data(dataset)
  .enter()
  .append("text")
  .attr("class", "MyText")
  .attr("transform", "translate(" + padding.left + "," + padding.top + ")")
  .attr("x", function(d, i) {
    return xScale(i) + rectPadding / 2 -10;
  })
  .attr("y", function(d) {
    return yScale(d);
  })
  .attr("dx", function() {
    return (xScale.step() - rectPadding) / 2;
  })
  .attr("dy", function(d) {
    return 20;
  })
  .text(function(d) {
    return d;
  });

//添加x轴
svg
  .append("g")
  .attr("class", "axis")
  .attr(
    "transform",
    "translate(" + padding.left + "," + (height - padding.bottom) + ")"
  )
  .call(xAxis);

//添加y轴
svg
  .append("g")
  .attr("class", "axis")
  .attr("transform", "translate(" + padding.left + "," + padding.top + ")")
  .call(yAxis);


开发一个稼动率的甘特图组件

codepen.io/Ricardo1601…

结合以上知识,写出如下代码


var width = 800;
var height = 200;
var rectPadding = 4;
//在 body 里添加一个 SVG 画布
var svg = d3
    .select("body")
    .append("svg")
    .attr("width", width)
    .attr("height", height);

var dataset = [{ eqp: 'eqp1', type: 'run', start: 2, end: 4 }, { eqp: 'eqp1', type: 'down', start: 0, end: 2 }, { eqp: 'eqp1', type: 'down', start: 4, end: 10 },
{ eqp: 'eqp2', type: 'run', start: 0, end: 9 }, { eqp: 'eqp2', type: 'down', start: 9, end: 10 }
];

var padding = { left: 20, right: 20, top: 20, bottom: 20 };

var yScale = d3
    .scaleBand()
    .domain(d3.range(2))
    .rangeRound([0, height - padding.top - padding.bottom]);

var yAxis = d3.axisBottom(yScale);

var xScale = d3
    .scaleLinear()
    .domain([0, 10])
    .range([0, width - padding.left - padding.right]);

var xAxis = d3.axisLeft(xScale);

svg.selectAll("rect")
    .data(dataset)
    .enter()
    .append("rect")
    .attr("transform", "translate(" + padding.left + "," + padding.top + ")")
    .attr("y", function (d, i) {
        if (d.eqp == "eqp1") {
            return yScale(0) + rectPadding / 2
        } else {
            return yScale(1) + rectPadding / 2
        }
    })
    .attr("x", function (d) {

        return xScale(d.start);
    })
    .attr("height", function () {
        return yScale.step() - rectPadding;
    })
    .attr("width", function (d) {
        return xScale(d.end - d.start);
    })
    .attr("fill", function (d) {
        if (d.type == "run") {
            return "green"
        } else {
            return "red"
        }
    })

image.png


官网示例学习

其实已经有大佬做出来需求的插件 github.com/flrs/visava…

image.png

在写Demo之前就看到过这个,但是本着学习的角度并没有参考源码或者直接拿来用,先自己实现一遍,再看一下大佬的思路,能更好的学习。