JavaScript数据可视化之D3

JavaScript数据可视化之D3

教程:

https://www.youtube.com/watch?v=_8V5o2UHG0E&list=WL&index=6&t=202s

网站:https://vizhub.com/

教程2:https://www.bilibili.com/video/BV1HK411L72d?p=1

源码: https://github.com/Shao-Kui/D3.js-Demos

文档:https://github.com/d3/d3/wiki

了解以下标签即可

<html>:最外层的主标签,每个HTML文件都需要有!

<head>: 标题内容,包含HTML的文件链接、标题等

<body>:HTML的主体,包括各种其中的各种元素

<title>:标题(显示在浏览器的标签栏)

<script>:JavaScript脚本或对于脚本的链接•D3.js的编程主要写在此标签中

<svg>: 对于D3最为重要的标签,主要操作的对象(见下页)

<link rel="stylesheet" href="/static/css/style.css">

<!DOCTYPEhtml>

•所有标签都需要以</tag>结束

D3: Data-Driven Documents

和python 的数据可视化和R语言不同,D3是把数据可视化展示在web上的,还可以创建用户交互等等

Loading D3.js

我们可以之间 在d3的文档上找 https://github.com/d3/d3/wiki/CN-Home

也可以在https://unpkg.com上找 引入d3 的script

直接<script src="https://d3js.org/d3.v5.min.js"></script>

或者<script src="https://unpkg.com/d3@5.6.0/dist/d3.min.js"></script>

也可以通过本地服务器链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
<title>Let's make a face with D3.js</title>
<link rel="stylesheet" href="style.css">
<script src="https://unpkg.com/d3@5.6.0/dist/d3.min.js"></script>
<!-- <script src="https://d3js.org/d3.v3.min.js"></script>
注意,这里要写https不要写http
-->
</head>
<body>
<svg width="960" height ="500">
<script>
const svg = d3.select('svg');
//我们把背景颜色设置为 red看看效果
svg.style('background-color','red');
</script>
</svg>
</body>
</html> <!DOCTYPE html>

对D3有一个大体的了解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
<head>
<title>Data Visualization!</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body>
<svg width="960" height="500" id="mainsvg" class="svgs"></svg>
<script>
//这段代码是在屏幕上绘制一个最简单的圆
let mainsvg = d3.select('.svgs');

let maingroup = mainsvg
.append('g')
.attr('transform', `translate(${100}, ${100})`);

let circle = maingroup
.append('circle')
.attr('stroke', 'black')
.attr('r', '66')
.attr('fill', 'yellow');
</script>
</body>
</html>

•在HTML中查找并获取SVG

•在SVG中加入组(Group)<g>

•设置组的平移为向下、向右个100像素

•在主要组中加入一个圆

•圆的边框为黑色

•圆的半径为66像素

•圆的填充颜色为黄色

Intro

HTML

Header Title

1
2
3
4
5
6
<!DOCTYPE html>
<html>
<head>
<title>HTML Cars Report</title>

</head>

Bulleted list

1
2
3
4
5
6
7
8
9
10

<body>
<h1>Cars Report</h1>
<ul>
<li class="highlighted">2012 Nissan Leaf: $1800</li>
<li>2009 Ford F150: $1950</li>
<li>2009 Chevrolet Trailblazer: $1550</li>
</ul>
</body>
</html> <!DOCTYPE html>

CSS

Customisze Font & Highlight an item

style.css 这是css选择器

1
2
3
4
5
6
7
body {
font-size: 2em;
font-family: monospace;
}
.highlighted{
color: red;
}

index.html中引入,用link标签

1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html>
<head>
<title>HTML Cars Report</title>
<link rel="stylesheet" href="style.css">
</head>
<!--<body...>-->
</html> <!DOCTYPE html>

SVG 可缩放矢量图形

SVG as Image Format

.svg 不会像素化 无论怎么放大,都是很清晰的

相反,png会像素化,放大会失真会模糊

SVG in HTML

coordinate system,circles,rectangles,lines, colors,groups

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!DOCTYPE html>
<html>
<head>
<title>Shapes with SVG and CSS</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<svg width="960" height ="500">
<!--
把所有的svg放在一个group中,整体设置大小为原来的1.5倍<g transform = "scale(1.5)">

第一行的圆形我们可以用cx和cy确定圆心坐标, r代表了半径
第二行的rectangle是正方形,x,y代表着他的左上角顶点坐标

第三行的圆我们添加了颜色, 用fill = "" 来定义颜色
第四行第五行我们可以在fill中加入多种格式的颜色 rgb(),#...

还可以把图片放到一个group里,通过group参数设置 transform="translate()"代表坐标,fill填充
如果group中的某一组件本身具有fill填充,会覆盖group中的fill

stroke代表边框或者线的颜色,stroke-width 代表线的粗细
我们可以定义<line></line>为一条线
我们还可以定义<path></path> 这是一条路线
M300 280代表我们move to(300,280) 这个点
L350 200代表我们画一条线从(300,280)指向(350,200)以此类推
-->
<g transform = "scale(1.5)">
<circle cx="50" cy="50" r="40"></circle>
<rect x="100" y="25" width = "50" height="50"></rect>
<circle cx="50" cy="150" r="40" fill="blue"></circle>
<rect x="100" y="125" width = "50" height="50" fill="#40b842"></rect>
<rect x="100" y="225" width = "50" height="50" fill="rgb(33, 14, 180)"></rect>
<g transform= "translate(0,200)" fill="pink" stroke="black">
<circle cx="50" cy="150" r="40" stroke-width="5" ></circle>
<rect x="100" y="125" width = "50" height="50"></rect>
</g>
<g class = "lines" transform="translate(50,0)">
<line x1="200" y1="20" x2="300" y2="280"></line>
<path fill="none" d="M300 280 L350 200 L450 250 L450 400"></path>
</g>
</g>
</svg>
</body>
</html> <!DOCTYPE html>

style.css 我们把lines定义成黑色,宽度为10,line中的path属性定义成这个颜色。

stroke-linejoin 属性指明路径的转角处使用的形状或者绘制的基础形状。developer.mozila.org

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
body { 
margin:0px;
overflow: hidden;
}
.highlighted{
color: red;
}
.lines{
stroke:black;
stroke-width:10;
}
.lines path{
stroke: #25656d ;
stroke-linejoin: round;
}

miter,round,bevel,miter-clip,arcs

Js D3的常用接口

•console.log(‘hello world! ’);

•数组a = [1, 2, 3]

•对象a = {name: ‘Shao-Kui’, age: 24.3, lab: ‘cscg’}

•D3数据可视化中常见对象数组,如:

​ •a = [{name: ‘Shao-Kui’, age: 24.3, dept: ‘cs’},

 •{name: ‘Wen-Yang’, age: 23, dept: ‘cs’},

​ •{name: ‘Yun’, age: 18😏, dept: ‘art’}]

•数组的排序a.sort()

•可加入回调函数来替代缺省的排序方案,如对日期排序

•a.sort(function(a,b){ return new Date(b.date) -new Date(a.date); }

•数组的查询a.find( d => d.name === ‘Wen-Yang’)

•把字符串转换成数值:+(‘3.14’)

•D3.js经常读取CSV文件,届时会涉及大量的数组、对象的操作!

Flask - Why?

•为什么需要一个后台(服务器)?

​ •CORS (Cross Origin Resource Sharing)

•可否用一个完整的.html文件编程

​ •完全可以,但数据处理、链接d3.js库、链接.js、链接.css文件会非常麻烦!

•对于没有WEB开发经验的同学,本课程对D3.js和可视化以外的工程不做要求:

​ •提供默认调通的客户端-服务器框架

​ •仅需要了解后续的内容,对服务器进行配置并运行即可

​ •推荐有web经验的同学搭建自己熟悉的环境

利用服务器我们可以设置路由,然后当我们输入链接会显示相应html文件

我们需要下载 flask_cors 涉及到跨域访问的问题.启动了这个flask后,我们输入 nCovnxt,显示了这样一个D3

我们可以很简单的在Flask中申请一个路由

1
2
3
@app.route('/stack_histogram')
def stack_histogram():
return flask.send_from_directory('static', 'stack_histogram.html')

Hello D3

D3获取修改删除节点

D3查询SVG

比如我们新建了这个demo,里面各个图形有他的id

除了基于单纯的id和class来进行索引,我们还可以通过层级查询来进行索引

比如下面有一堆父节点,那么我们先用 #maingroup找到父节点,然后再在里面找到rect子节点

或者我找到所有 class = “tick” 的父节点,然后去下面找text的子节点

class索引某个子集,id索引某个特定的元素

元素的 Class :人为赋予的“类别”可以标记元素集合, 其中的元素标签可以不相同 !

比如我可以利用class 筛选这rect1,rect2,circle1 这三个图型,因为他们的class相同 ,id可以不同

使用D3 设置SVG中的属性

尽可能记住常用的属性

现查现用!https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute

比如我们在chrome中修改一个柱体,那么我们先 选择rect2,然后利用attr把柱体的颜色改成黑色

利用selectAll把一个类中的所有对象全部选择,然后填充黄色

transform

translate

rotate

scale

element.attr

前面的参数是图元的属性名称,后面一个是要设置的值

活用 \节点

子节点的属性设置都是在父节点的基础上进行设置

添加和删除元素

append后面写的是添加什么类型的图元

1
d3.select('#maingroup').append('rect').attr('width',100).attr('height',100);

我如果想删除一个图元,那么

1
d3.select('#rect1').remove();

无论是添加或还是删除,都需要先查询,再做操作

margin

首先还要了解一下margin

margin就是边框

SVG对于D3.js是一个“画布”,SVG范围外的任何内容属于画布之外,浏览器将不予显示

定义Margin:

1
const margin = {top: 60, right: 30, bottom: 60, left: 200}

计算实际操作的 inner 长/宽

1
2
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

在SVG下额外定义一个组作为新的根节点

1
2
const g = svg.append('g').attr('id', 'maingroup')
attr('transform', `translate(${margin.left}, ${margin.top})`);

比例尺

• 比例尺用于把实际的数据空间映射到屏幕的空间
• 比例尺非常重要,会经常同时传给坐标轴与数据

线性的比例尺

1
2
3
4
5
6
7
8
9
const xScale = d3.scaleLinear()
//domain就是原比例
.domain([min_d, max_d]).
//range就是缩放后的比例尺
//range和domain都必须传入一个数组
.range([min, max]);
const xScale = d3.scaleLinear()
.domain([0, d3.max(data, datum=>datum.value)])
.range([0, innerWidth]);

d3.max(数据,回调:如何提取数据的值)
d3.max: 求出数据某一属性的最大值,比如年龄的最大值

from Data Space to Screen Space

柱状的比例尺 :对应离散的变量

1
2
3
4
5
6
7
8
const yScale = d3.scaleBand() 
.domain(list)
.range([min, max])
.padding(p);
const yScale = d3.scaleBand()
.domain(data.map(datum => datum.name))
.range([0, innerHeight])
.padding(0.1);
  • 通过d3.scaleLinear或d3.scaleBand得到的返回值本质上是函数
    • 给出数据中的值(domain)
    • 返回映射后的值(range)
    • .domain(…)和.range(…)可以理解为配置这个函数(功能)的过程
  • 比例尺的定义仍适用链式调用
  • 坐标轴实际上是一个映射函数,我可以把我domain中的连续的或者离散的数据映射到图标当中

坐标轴

引入坐标轴

  • 一个坐标轴为一个group(<g>),通常我们需要两个坐标轴
  • 坐标轴中包含:
    • 一个 <path>用于横跨坐标轴的覆盖范围
    • 若干个刻度
      • 每个刻度也是一个group
    • 每个刻度下属还会包含一个<line> 和一个<text>
      • <line> 用于展示坐标轴的轴线,如左到右或上到下
      • <text> 用于展示坐标轴的刻度值,如实数、姓名、日期
    • (可选)一个标签用以描述坐标轴
  • 定义坐标轴(获得结果是函数):
    • const yAxis = d3.axisLeft(yScale);
    • const xAxis = d3.axisBottom(xScale);
    • axisLeft: 左侧坐标轴
    • axisBotton: 右侧坐标轴
  • 实际配置坐标轴:
    • const yAxisGroup = g.append(‘g’).call(yAxis);
    • const xAxisGroup = g.append(‘g’).call(xAxis)
  • 实际配置后会发现 <g>中增添了与坐标轴相关的元素
  • 任何坐标轴在初始化之后会默认放置在坐标原点,需要进一步的平移
  • 关于 selection.call(…)

    • 函数的输入为另一个函数
    • 另一个函数以selection的图元作为输入
    • 另一个函数中会根据函数体的内容修改selection对应的图元
  • 下面的代码的意思就是告诉坐标轴他该怎么使用这个而比例尺
    • const yAxis = d3.axisLeft(yScale);
    • const yAxisGroup = g.append(‘g’).call(yAxis);
  • ​ 对函数式编程敏感有助于学习D3 & 理解D3的代码!

配置坐标轴

  • 可以对坐标轴的风格进行修改:d3.selectAll(‘.tick text’).attr(‘font-size’, ‘2em’);
  • .tickSize来设置网格线
  • 坐标轴的标签加入不在D3接口的负责范围内:
    • 通过对应组 .append(‘text’)来人为实现
    • (左)纵轴坐标需要 .attr(‘transform’, ‘rotate(-90)’) 来旋转
    • 纵轴坐标旋转后,x / y 会颠倒甚至取值范围相反
    • 回忆DOM:父节点的属性会影响子节点,而坐标轴默认的’fill’属性是 ‘none’,因此请一定手动设置文字颜色 .attr(‘fill’, ‘black’)

Data-Join

我们能不能把添加和修改图元自动化?

  • Data-Join 的本质上 是将数据与图元进行绑定
    • 每个国家的人数绑定到矩形的长度
    • 疫情感染的人数比例绑定到圆的半径
  • Why?

    • 使用Data-Join可以省去大量根据数据设置图元属性的代码量
    • 对于动态变化的数据提供统一接口
  • 以数据为中心的可视化操作
    • 根据数据的每个属性自动调整绑定图元的属性

  • 不再需要手动添加、‘修改’、删除图元
    • 会根据Data-Join的绑定自动推断
  • 如果图元的数目不等于数据的条目?
    • 根据数据条目的数量选定相应数量的图元

用函数设置图元属性

  • 回忆使用.attr设置图元属性
  • selection.attr(‘attrbuteName’, ‘value’)
    • 支持直接通过值来设置属性
    • 支持通过函数来设置属性
  • selection.attr(‘attrbuteName’, (d, i) => {…})
    • d为绑定给图元的数据(即将到来)
    • i为图元的索引,是一个整数,如d3.selectAll(‘rect’)中的第几个矩形
    • 函数也可以仅使用 d => {…},但此时函数体无法使用索引
    • 即使不使用绑定的数据(如没有绑定数据),如需使用索引,仍需要完整的写出 (d, i) => {…}

数据与图元的绑定

  • .data( dataArray )
  • dataArray在保证是一个数组的前提下可以是任何形式
    • e.g., [0, 2, 5, 6, 233, 666, 384, 32, 18]
    • e.g., [{name: ‘Sebastian’, value:384}, {name:’ Ciel’, value:32},{name:’Cai Yun’, value:16}]
  • 先考虑数据和图元数目相同的情况:
    • dataArray是一个数组,其中的每‘条’数据会与一个图元绑定(反之亦然)
  • 绑定给每个图元的数据将对应 .attr(, (d, i) => {…}) 中的 d
  • 默认的绑定按照双方的索引顺序
  • 不调用.data(…),则图元不会与任何数据绑定!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
      const data = [{name: 'Shao-Kui', value:6},
{name:'Wen-Yang', value:6}, {name:'Cai Yun', value:16}, {name:'Liang Yuan', value: 10},
{name:'Yuan-Chen', value:6}, {name:'Rui-Long', value:10}, {name:'Dong Xin', value:12},
{name:'He Yu', value:20}, {name:'Xiang-Li', value:12}, {name:'Godness', value:20},
{name:'Wei-Yu', value:15}, {name:'Chen Zheng', value:14},
{name:'Yu Peng', value:15}, {name:'Li Jian', value:18}];

const data1 = [{name: 'Shao-Kui', value:12},
{name:'Wen-Yang', value:13}, {name:'Cai Yun', value:16}, {name:'Liang Yuan', value: 10},
{name:'Yuan-Chen', value:6}, {name:'Rui-Long', value:10}, {name:'Dong Xin', value:12},
{name:'He Yu', value:20}, {name:'Xiang-Li', value:12}, {name:'Godness', value:20},
{name:'Wei-Yu', value:15}, {name:'Chen Zheng', value:14},
{name:'Yu Peng', value:15}, {name:'Li Jian', value:18}];

const data2 = [{name: 'Wen-Yang', value:15},
{name:'Shao-Kui', value:20}, {name:'Cai Yun', value:16}, {name:'Yuan-Chen', value: 10},
{name:'Liang Yuan', value:6}, {name:'Rui-Long', value:10}, {name:'Dong Xin', value:12},
{name:'He Yu', value:20}, {name:'Xiang-Li', value:12}, {name:'Godness', value:20},
{name:'Wei-Yu', value:15}, {name:'Chen Zheng', value:14},
{name:'Yu Peng', value:15}, {name:'Li Jian', value:18}];

如上面我定义了data1,data2,我如果想把data1中的数据渲染在图表上去替换data中的数据

1
d3.selectAll('rect').data(data1).attr('width',d=> xScale(d.value));

但是我如果这样来改,就会出现问题了。因为默认的data函数根据index来索引的,如果我给出的数据的顺序发生了变化,我们还是从第一个到最后一个去索引

那么这样data2中Wen-Yang的数据就会绑定到Shao-Kui上去。

1
d3.selectAll('rect').data(data1).attr('width',d=> xScale(d.value));

Key

  • .data(data, keyFunction)
    • keyFunction的返回值通常是一个字符串(string)
    • keyFunction的定义根据数据,比如 keyFunction = d => d.name
  • 在绑定数据给图元时:
    • keyFunction为每条输入绑定的数据执行一次
    • keyFunction为每个包含数据的图元执行一次
  • 如果图元之前没有绑定过任何数据,则keyFunction会报错!
    • 第一次绑定时根据索引即可
    • 实际的可视化任务,图元都是根据数据的‘条’数动态添加(enter)、删除(exit),只需要在添加时指定好DOM的ID即可

所以我们需要手动的把key值加到数据中去。

这里,因为数据中的name都是不一样的,所以我们可以把name设置为key :data(data2,d=>d.name)

1
d3.selectAll('rect').data(data2,d=>d.name).attr('width',d=> xScale(d.value));

现在的值就变得正常了。

也就是用data来绑定数据,用key来进行索引

Enter Update Exit

  • D3.js绑定数据的三个‘状态’
  • Update
    • 图元和数据条目相同,之前的介绍均为单纯的update
  • Enter
    • 数据的条目多于图元甚至没有图元,常用于第一次绑定数据
  • Exit
    • 数据的条目少于图元甚至没有数据,常用于结束可视化

Enter

  • 有数据没图元
  • D3.js会自动‘搞清楚’哪些数据是新增的
  • 根据新增的数据生成相应的图元
  • 生成图元的占位,占位的内容需要编程者自行添加(append)
  • const p = maingroup.selectAll(‘.class’).data(data).enter().append(‘’).attr(…)
    • 相当于我调用data的时候,selectAll中的内容都是空的,然后我们可以向里面添加什么样的图元,设置什么样的属性
  • enter本质上生成指向父节点的指针,而append操作相当于在父节点后添加指针数量的图元并将其与多出的数据绑定
1
2
3
4
5
6
g.selectAll('.dataRect').data(data).enter().append('rect')
.attr('class', 'dataRect')
.attr('width', d => xScale(d.value))
.attr('height', yScale.bandwidth())
.attr('y', d => yScale(d.name))
.attr('fill', 'green').attr('opacity

Update

  • 有图元有数据
  • const p = maingroup.selectAll(‘.datacurve’).data(data).attr(…).attr(…)
  • Update作为实际可视化任务最常用的状态,经常被单独封装成一个函数
  • updateSelection.merge( enterSelection ).attr(…).attr(…)
    • 将两个selection合并到一起操作
    • enterSelection需要至少append(…)图元

Exit

  • 有图元没数据
    D3.js会自动‘搞清楚’哪些图元是不绑定数据的
  • 引用官方文档:existing DOM elements in the selection for which no new datum was found
  • const p = maingroup.selectAll(‘.class’).data(data).exit().remove()

Animation

  • Update经常与D3.js的动画一起使用
  • .transition().duration()
    • transition就是说进入动画模式, duration()是设置动画持续时间
  • d3.selectAll(‘rect’).data(data2, d => d.name).transition().duration(3000).attr(‘width’, d => xScale(d.value))
  • .duration(…)中为毫秒,即3000表示3秒钟
  • .transition(…)经过调用后,后续的链式调用会变成数值上的渐变,渐变的时间由.duration(…)设定
  • 插值的方式由.ease(…)设定
1
2
3
d3.selectAll('rect').data(data).transition().duration(1000).attr('width',d=>xScale(d.value));
d3.selectAll('rect').data(data1).transition().duration(1000).attr('width',d=>xScale(d.value));
d3.selectAll('rect').data(data2,d=>d.name).transition().duration(1000).attr('width',d=>xScale(d.value));

结果如下

Data-Join简洁形式

  • data(…).join(‘…’)
  • 默认enter和update的执行形式相同
  • 默认exit是删除(remove)节点
  • 默认data-join形式简洁但不灵活
    • 必须需要设置enter数据的初始图元属性,update会每次重新设置初始值,从而导致动画出现‘奇怪’的效果
    • 仍支持‘定制’:
    • .join(enter => enter.append(“text”).attr(“fill”, “green”).text(d => d),update => (), exit => ())
1
2
d3.selectAll('rect').data(data).transition().duration(1000).attr('width',d=>xScale(d.value));
d3.selectAll('rect').data(data1).join('rect').transition().duration(1000).attr('width',d=>xScale(d.value));

数据的读取

常见的CSV数据

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

读取

  • d3.csv(‘path/to/data.csv’).then( data => {…} ) 对数据进行预处理
  • .csv函数的返回值是一个JS的’Promise’对象
    • ‘Promise’对象用于执行异步操作
  • ​ .then(…)的参数为一个函数,参数为.csv(…)的返回值
    • 实际上在JavaScript异步中是一个resolve
  • d3.csv(…)会正常向服务器请求数据,在请求并处理好之后,将结果扔给.then(…)中的回调函数

实例:绘制笑脸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import {select,arc} from 'd3';
const svg = select('svg');


const width = +svg.attr('width');
const height = +svg.attr('height');
const g = svg
.append('g')
.attr('transform',`translate(${width/2},${height/2})`)
const circle = g
.append('circle')
.attr('r',height/2)
.attr('fill','yellow')
.attr('stroke','black')
const eyeSpacing = 100;
const eyeYOffset = -70;
const eyeRadius = 40;
const eyebrowWidth = 70;
const eyebrowHeight =15;
const eyebrowYOffset = -70;
//眼睛组
const eyesG = g
.append('g')
.attr('transform',`translate(0,${eyeYOffset})`);
const leftEye = eyesG
.append('circle')
.attr('r',eyeRadius)
.attr('cx', -eyeSpacing)
const rightEye = eyesG
.append('circle')
.attr('r',eyeRadius)
.attr('cx', eyeSpacing)
//眉毛组,我觉得可以说是从眼睛组中继承过来的。
const eyebrowsG = eyesG
.append('g')
.attr('transform',`translate(0,${eyeYOffset})`);
eyebrowsG
.transition().duration(2000)
.attr('transform',`translate(0,${eyebrowYOffset-50})`)
.transition().duration(2000)
.attr('transform',`translate(0,${eyebrowYOffset})`);

//左右眉毛
const leftEyebrow = eyebrowsG
.append('rect')
.attr('x', -eyeSpacing - eyebrowWidth / 2)
.attr('width', eyebrowWidth)
.attr('height', eyebrowHeight);

const rightEyebrow = eyebrowsG
.append('rect')
.attr('x', eyeSpacing - eyebrowWidth / 2)
.attr('width', eyebrowWidth)
.attr('height', eyebrowHeight);

//嘴巴,利用arc,是制作弧的一个api
const mounth = g
.append('path')
.attr('d',arc()({
innerRadius: 150,
outerRadius: 170,
startAngle: Math.PI/2,
endAngle: Math.PI*1.5 ,
}))

实例: 绘制柱状图

画处柱形图需要遵循五个步骤

  • Representing a data dable in JavaScript

data.css

1
2


1
2
3
4
5
6
7
8
9
10
11
import { select, csv, max, scaleLinear, scaleBand } from "d3";
const svg = select("svg");

const width = +svg.attr("width");
const height = +svg.attr("height");
csv("data.csv").then((data) => {
data.forEach((d) => {
d.population = +d.population * 1000;
});
render(data);
});
  • Creating rectangles for each row
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//...
const render = (data) => {
const xValue = (d) => d.population;
const yValue = (d) => d.country;
svg
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("y", (d) => yScale(yValue(d)))
.attr("width", (d) => xScale(xValue(d)))
.attr("height", yScale.bandwidth());
};
//...
  • Using linear and band scales
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//...
const render = (data) => {
const xValue = (d) => d.population;
const yValue = (d) => d.country;
const xScale = scaleLinear()
.domain([0, max(data, xValue)])
.range([0, width]);

const yScale = scaleBand()
.domain(data.map(yValue))
.range([0, height])
.padding(0.1);
svg
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("y", (d) => yScale(yValue(d)))
.attr("width", (d) => xScale(xValue(d)))
.attr("height", yScale.bandwidth());
};
//...
  • The margin convention

  • Adding axes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import {
select,
csv,
max,
scaleLinear,
scaleBand,
axisLeft,
axisBottom,
} from "d3";
const svg = select("svg");

const width = +svg.attr("width");
const height = +svg.attr("height");

const render = (data) => {
const xValue = (d) => d.population;
const yValue = (d) => d.country;
const margin = { top: 20, right: 40, bottom: 20, left: 100 };
const innerheight = width - margin.left - margin.right;
const innerwidth = width - margin.left - margin.right;

//比例尺
const xScale = scaleLinear()
.domain([0, max(data, xValue)])
.range([0, innerwidth]);

const yScale = scaleBand()
.domain(data.map(yValue))
.range([0, innerheight])
.padding(0.1);

const g = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
//坐标轴
g.append("g").call(axisLeft(yScale));
g.append("g")
.call(axisBottom(xScale))
.attr("transform", `translate(0,${innerheight})`);


g.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("y", (d) => yScale(yValue(d)))
.attr("width", (d) => xScale(xValue(d)))
.attr("height", yScale.bandwidth());
};
csv("data.csv").then((data) => {
data.forEach((d) => {
d.population = +d.population * 1000;
});
render(data);
});

Customizing Axes

  • Formatting numbers

http://bl.ocks.org/zanarmstrong/05c1e95bf7aa16c4768e

1
2
3
4
5
6
//默认 百万M上面是G,但是我们认为十亿级的更准确,所以我们用replace函数把 G换成了B。然后用
//tickFormat来引用我们自定义的格式化规则
const xAxisTickFormat = (number) => format(".3s")(number).replace("G", "B");
const xAxis = axisBottom(xScale).tickFormat(xAxisTickFormat);
g.append("g").call(axisLeft(yScale));
g.append("g").call(xAxis).attr("transform", `translate(0,${innerheight})`);

  • Removing unnecessary lines

我们要去除这些根难看的边框线和凸起来的东西

1
2
3
4
5
6
7
8
9
g.append("g")
.call(axisLeft(yScale))
.selectAll(".domain,.tick line")
.remove();
g.append("g")
.call(xAxis)
.attr("transform", `translate(0,${innerheight})`)
.selectAll(".domain ")
.remove();

  • Adding a Visualization title
1
g.append("text").attr("y", -10).text("Top 10 Most Popular Countries");

  • Adding axis labels
1
2
3
4
5
6
7
8
9
10
11
const xAxisG = g
.append("g")
.call(xAxis)
.attr("transform", `translate(0,${innerheight})`);
xAxisG.select(".domain ").remove();
xAxisG
.append("text")
.attr("fill", "black")
.attr("y", 60)
.attr("x", innerWidth / 2 - 100)
.text("Population");

  • Making tick grid lines网格线

css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
body {
margin:0px;
overflow: hidden;
}

rect {
fill: steelblue;
}
text{
font-size :3em;
font-family: sans-serif;
}
.tick text {
font-size :2.7em;
fill: #635F5D;
}
.tick line{
stroke: #C0C0BB
}
.axis-label{
font-size: 5em;
fill: #8E8883
}
.axis-title{
font-size: 3.2em;
fill:#635F5D;
}

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import { 
select,
csv,
scaleLinear,
max,
scaleBand,
axisLeft,
axisBottom,
format
} from 'd3';



const render = (data) => {
const svg = select('svg')
const height = +svg.attr('height')
const width = +svg.attr('width')

const countryMapping = d => d.country
const populationMapping = d => d.population

const countries = data.map(countryMapping)
const populationMax = max(data, populationMapping)
const margin = {
top: 45,
bottom: 85,
left: 200,
right: 100
}
const textHeight = 10
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom - textHeight
const xScale = scaleLinear().domain([0, populationMax]).range([0, innerWidth])

const yBandScale = scaleBand()
.domain(countries)
.range([0, innerHeight])
.padding(.2)

const barGroup = svg.append('g').attr('transform',
`translate(${margin.left}, ${margin.top + textHeight + 5})`)


const yAxis = axisLeft(yBandScale)

const xAxisTickFormat = number => {
return format('.3s')(number).replace('G', 'B')
}
const xAxis = axisBottom(xScale)
.tickFormat(xAxisTickFormat)

barGroup.append('g').call(yAxis)
.selectAll('.domain, .tick line')
.remove();


const xAxisG = barGroup.append('g').call(xAxis)
.attr('transform', `translate(${0}, ${innerHeight})`);

xAxisG
.append('text')
.text('Something is missing')
.attr('y', 50)
.attr('x', innerWidth / 2)
.attr('class', 'x-label')
.text("Population");

xAxisG
.append("text")
.attr("class", "axis-label")//加上一个类标签,供css选择器去选择
.attr("fill", "black")
.attr("y", 75)
.attr("x", innerWidth / 2 )
.text("Population");
barGroup
.selectAll('.domain')
.remove();

barGroup
.append("text")
.attr("class", "axis-title")//加上一个类标签,供css选择器去选择
.attr("y", -10)
.text("Top 10 Most Popular Countries");

barGroup
.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('y', d => yBandScale(countryMapping(d)))
.attr('width', d => xScale(populationMapping(d)))
.attr('height', yBandScale.bandwidth())

svg.append("text")
.attr("class", "axis-title")//加上一个类标签,供css选择器去选择
.attr("y", -10)
.text("Top 10 Most Popular Countries");


}

csv('data.csv').then(data => {
data.forEach(record => {
record.population = +record.population * 1000
})
render(data)
})

实例:绘制柱状图

效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<!DOCTYPE html>
<html>
<head>
<title>BarChart! </title>
<script src="../js/d3.min.js"></script>
</head>
<body>
<!--这里我已经定义了svg画布的宽和高-->
<svg width="1600" height="800" id="mainsvg" class="svgs" ></svg>
<script>
//这是我们想展示的内容,是一个数组
const data = [{name: 'Shao-Kui', value:6},
{name:'Wen-Yang', value:6}, {name:'Cai Yun', value:16}, {name:'Liang Yuan', value: 10},
{name:'Yuan-Chen', value:6}, {name:'Rui-Long', value:10}, {name:'Dong Xin', value:12},
{name:'He Yu', value:20}, {name:'Xiang-Li', value:12}, {name:'Godness', value:20},
{name:'Wei-Yu', value:15}, {name:'Chen Zheng', value:14},
{name:'Yu Peng', value:15}, {name:'Li Jian', value:18}];
//首先我们获得上面定义的svg画布
const svg = d3.select('#mainsvg');
//获取svg的长和宽
const width = +svg.attr('width');
const height = +svg.attr('height');
//定义预留空间
const margin = {top: 60, right: 30, bottom: 60, left: 150};
//真正作画的空间
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
/*
从上面可以看到横轴是连续的,所以我们用scaleLiner()比例尺进行映射
怎么映射呢?
数据的最小值就是0,数据得最大值就利用d3.max ,第一个数据就是数据本身,第二个参数就是一个函数
这个函数会告诉我们数据是怎么获取到的
映射范围就是0到刚才定义的内边框的宽
*/
const xScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, innerWidth]);
/*
数轴是名字,所以是离散的,我们用scaleBand()比例尺进行映射
怎么映射band呢?
我们利用js中的map函数,传入一个回调函数,传回一个数组。并把这个数组给domain
间隙为0.1
*/
const yScale = d3.scaleBand()
.domain( data.map(d => d.name) )
.range([0, innerHeight])
.padding(0.1);
/*
在渲染坐标轴之前我们要把整个柱形图提供给一个容器
我们定义一个g标签,id就是我们的maingroup,
然后设置transform属性,把这个柱形图平移到我们刚才规划的那一片区域,利用模板语法
*/
const g = svg.append('g').attr('id', 'maingroup')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
/*
然后我们在maingroup中添加坐标轴的容器了
定义坐标轴,告诉坐标轴的取值范围该如何映射
然后我们向里面添加横轴数值和竖轴的人名,注意写法, call(xAxis)就是把group交给xAxis函数去处理
xAxis
但是坐标轴会默认定义在上面,如果我们要把横轴定义在最底端,我们需要平移一下
*/
const xAxis = d3.axisBottom(xScale);
g.append('g').call(xAxis).attr('transform', `translate(${0}, ${innerHeight})`);

const yAxis = d3.axisLeft(yScale);
g.append('g').call(yAxis);
/*
现在对每一条数据都要画出一个条带,也就是一个矩形
因为我们是根据横轴比例尺定义长度,那么width 就是数据的value属性
高度就是利用yScale提供的方法,交给D3自己去定义
然后我们填充一个颜色
最后我们需要让信息中的人名和坐标轴上的人名一一对应,这样才能映射上去
*/
data.forEach( d => {
g.append('rect')
.attr('width', xScale(d.value))
.attr('height', yScale.bandwidth())
.attr('fill', 'green')
.attr('y', yScale(d.name))
} );
/*
现在我们需要对柱状图进行一些改进,这里我们想把纵坐标的刻度上的文本,也就是人名
那么我们把他的字体大小设置成2em
最后我们希望加一个表头,也就是标题
那么我们用append一个text,然后利用.text()设置文字内容,字体大小为3em
然后我们需要把他居中,利用transform属性,但还不够,我们现在只是把文本的开头移动到了中间
最后我们设置文字的锚设置为中间。现在文本的中间就在画布的中间了,达成了居中的效果
*/
d3.selectAll('.tick text').attr('font-size', '2em');
g.append('text').text('Members of CSCG').attr('font-size', '3em')
.attr('transform', `translate(${innerWidth/2}, ${0})`)
.attr('text-anchor', 'middle');

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

实现一个散点图-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import {
select,
csv,
scaleLinear,
max,
scalePoint,
axisLeft,
axisBottom,
format,
} from "d3";

const render = (data) => {
const svg = select("svg");
const height = +svg.attr("height");
const width = +svg.attr("width");

const countryMapping = (d) => d.country;
const populationMapping = (d) => d.population;

const countries = data.map(countryMapping);
const populationMax = max(data, populationMapping);
const margin = {
top: 45,
bottom: 85,
left: 200,
right: 100,
};
const textHeight = 10;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom - textHeight;
const xScale = scaleLinear()
.domain([0, populationMax])
.range([0, innerWidth])
.nice();

const yScale = scalePoint()
.domain(countries)
.range([0, innerHeight])
.padding(0.7);

const circleGroup = svg
.append("g")
.attr(
"transform",
`translate(${margin.left}, ${margin.top + textHeight + 5})`
);

const yAxis = axisLeft(yScale).tickSize(-innerWidth);

const xAxisTickFormat = (number) => {
return format(".3s")(number).replace("G", "B");
};
const xAxis = axisBottom(xScale)
.tickFormat(xAxisTickFormat)
.tickSize(-innerHeight);

circleGroup.append("g").call(yAxis).selectAll(".domain").remove();

const xAxisG = circleGroup
.append("g")
.call(xAxis)
.attr("transform", `translate(${0}, ${innerHeight})`);

xAxisG
.append("text")
.text("Something is missing")
.attr("y", 50)
.attr("x", innerWidth / 2)
.attr("class", "x-label")
.text("Population");

xAxisG
.append("text")
.attr("class", "axis-label") //加上一个类标签,供css选择器去选择
.attr("fill", "black")
.attr("y", 75)
.attr("x", innerWidth / 2)
.text("Population");
circleGroup.selectAll(".domain").remove();

circleGroup
.append("text")
.attr("class", "axis-title") //加上一个类标签,供css选择器去选择
.attr("y", -10)
.text("Top 10 Most Popular Countries");

circleGroup
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cy", (d) => yScale(countryMapping(d)))
.attr("cx", (d) => xScale(populationMapping(d)))
.attr("r", 18);

svg
.append("text")
.attr("class", "axis-title") //加上一个类标签,供css选择器去选择
.attr("y", -10)
.text("Top 10 Most Popular Countries");
};

csv("data.csv").then((data) => {
data.forEach((record) => {
record.population = +record.population * 1000;
});
render(data);
});

实现一个散点图-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import {
select,
csv,
scaleLinear,
extent,
axisLeft,
axisBottom,
format
} from 'd3';

// d.mpg = +d.mpg;
// d.cylinders = +d.cylinders;
// d.displacement = +d.displacement;
// d.horsepower = +d.horsepower;
// d.weight = +d.weight;
// d.acceleration = +d.acceleration;
// d.year = +d.year;
const svg = select('svg');

const width = +svg.attr('width');
const height = +svg.attr('height');

const render = data => {
const title = 'Cars: Horsepower vs. Weight';
//可以通过改变xValue和yValue来做不同的图
const xValue = d => d.horsepower;
const xAxisLabel = 'Horsepower';

const yValue = d => d.weight;
const circleRadius = 10;
const yAxisLabel = 'Weight';

const margin = { top: 60, right: 40, bottom: 88, left: 150 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
/*******************x和y的比例尺********************/
const xScale = scaleLinear()
//extent(data, xValue)函数就是[min(data,xValue),max(data,xValue)]
.domain(extent(data, xValue))
.range([0, innerWidth])
.nice();

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

const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
/******************x和y 的坐标轴*********************/
const xAxis = axisBottom(xScale)
.tickSize(-innerHeight)
//让坐标轴下面的数标和轴远离
.tickPadding(15);

const yAxis = axisLeft(yScale)
.tickSize(-innerWidth)
.tickPadding(10);
/****************y坐标轴的属性*******************/
const yAxisG = g.append('g').call(yAxis);
yAxisG.selectAll('.domain').remove();
//y轴的名称
yAxisG.append('text')
.attr('class', 'axis-label')
.attr('y', -93)
.attr('x', -innerHeight / 2)
.attr('fill', 'black')
//反转 Weight -90度
.attr('transform', `rotate(-90)`)
//把锚放中间,达到剧中效果
.attr('text-anchor', 'middle')
.text(yAxisLabel);
/*****************x坐标轴的属性*********************/
const xAxisG = g.append('g').call(xAxis)
.attr('transform', `translate(0,${innerHeight})`);

xAxisG.select('.domain').remove();

xAxisG.append('text')
.attr('class', 'axis-label')
.attr('y', 75)
.attr('x', innerWidth / 2)
.attr('fill', 'black')
.text(xAxisLabel);
/**************************************************/
g.selectAll('circle').data(data)
.enter().append('circle')
.attr('cy', d => yScale(yValue(d)))
.attr('cx', d => xScale(xValue(d)))
.attr('r', circleRadius);
/***************设置名称***************************/
g.append('text')
.attr('class', 'title')
.attr('y', -10)
.text(title);
};

csv('https://vizhub.com/curran/datasets/auto-mpg.csv')
.then(data => {
data.forEach(d => {
d.mpg = +d.mpg;
d.cylinders = +d.cylinders;
d.displacement = +d.displacement;
d.horsepower = +d.horsepower;
d.weight = +d.weight;
d.acceleration = +d.acceleration;
d.year = +d.year;
});
render(data);
});

用自己爬取的数据做的两个图标.只需要载入数据,模板是一样的,方便

-------------本文结束,感谢您的阅读-------------