前言
D3
近年来一直是 JavaScript
最重要的数据可视化库之一,在创建者 MikeBostock
的维护下,前景依然无量,至少现在没有能打的:
D3
与众多其他库的区别在于无限定制的能力(直接操作SVG
)。- 它的底层
API
提供对原生SVG
元素的直接控制,但它也带来了高学习曲线的成本。 - 我们将把
D3
和Vue
结合在一起 - 使用Vue
的动态数据绑定,清晰的语法和模块化结构,可以充分发挥D3
的最佳性能。
根据广泛定义,D3可拆分为以下几种分库:
- 绝大部分的
D3
课程或书籍,都会着重讲解在其DOM
操作功能上,但这明显与近几年来的web框架理念相违背。 - 用于数据可视化的
D3
,其核心在于使用绘图指令装饰数据,从源数据创建新的可绘制数据,生成SVG
路径以及从数据和方法在DOM
中创建数据可视化元素(如轴)的功能。
- 有许多用于管理DOM的工具,所有这些工具都可以在
D3
中集成数据可视化功能。这也是D3
能与Vue
无缝结合的原因之一。
于此,我们不需要从 D3 DOM
操作功能开始学起,直接通过实例来入门 D3
。
D3.js 渐进入门
以下实例的模版均为以下形式:
代码语言:javascript复制<html>
<head>
<link rel="stylesheet" href="index.css">
<title>Learn D3.js</title>
</head>
<body>
<!--或其它标签-->
<h1>First heading</h1>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="index.js"></script>
</body>
</html>
1. 选择和操作
你需要学习的第一件事是如何使用D3.js选择和操作DOM元素。该库在操作DOM方面实际上非常强大,因此理论上可以将其用作 jQuery
的替代品。以下代码请逐行添加运行。
// index.js
d3.select();
d3.selectAll();
d3.select('h1').style('color', 'red')
.attr('class', 'heading')
.text('Updated h1 tag');
d3.select('body').append('p').text('First Paragraph');
d3.select('body').append('p').text('Second Paragraph');
d3.select('body').append('p').text('Third Paragraph');
d3.selectAll('p').style('')
2.数据加载和绑定
当你要创建可视化时,了解如何加载数据以及将其绑定到DOM非常重要。所以在这个实例中,你将学到这两点。
代码语言:javascript复制let dataset = [1, 2, 3, 4, 5];
d3.select('body')
.selectAll('p')
.data(dataset)
.enter()
.append('p') // appends paragraph for each data element
.text('D3 is awesome!!');
//.text(function(d) { return d; });
3.创建一个简单的柱状图
首先需要添加一个 svg
标签
<h1>Bar Chart using D3.js</h1>
<svg class="bar-chart"></svg>
然后在 index.js
中添加(已添加关键注释):
// 数据集
let dataset = [80, 100, 56, 120, 180, 30, 40, 120, 160];
// 定义svg图形宽高,以及柱状图间距
let svgWidth = 500, svgHeight = 300, barPadding = 5;
// 通过图形计算每个柱状宽度
let barWidth = (svgWidth / dataset.length);
// 绘制图形
let svg = d3.select('svg')
.attr("width", svgWidth)
.attr("height", svgHeight);
// rect,长方形
// 文档:http://www.w3school.com.cn/svg/svg_rect.asp
let barChart = svg.selectAll("rect")
.data(dataset) //绑定数组
.enter() // 指定选择集的enter部分
.append("rect") // 添加足够数量的矩形
.attr("y", d => svgHeight - d ) // d为数据集每一项的值, 取y坐标
.attr("height", d => d) // 设定高度
.attr("width", barWidth - barPadding) // 设定宽度
.attr("transform", (d, i) => {
let translate = [barWidth * i, 0];
return "translate(" translate ")";
}); // 实际是计算每一项值的x坐标
4. 在图形上方显示数值
这时就需要在上述代码中创建 svg
的 text
文本
let text = svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(d => d)
.attr("y", (d, i) => svgHeight - d - 2)
.attr("x", (d, i) => barWidth * i)
.attr("fill", "#A64C38");
过程比较简单,就是返回文本,计算x/y坐标,并填充颜色。
5. scales
: 比例尺函数
D3
中有个重要的概念就是比例尺。比例尺就是把一组输入域映射到输出域的函数。映射就是两个数据集之间元素相互对应的关系。比如输入是1,输出是100,输入是5,输出是10000,那么这其中的映射关系就是你所定义的比例尺。
D3
中有各种比例尺函数,有连续性的,有非连续性的,在本例子中,你将学到 d3.scaleLinear()
,线性比例尺。
5.1 d3.scaleLinear()
,线性比例尺
使用 d3.scaleLinear()
创造一个线性比例尺,其中:
domain()
是输入域range()
是输出域- 相当于将
domain
中的数据集映射到range
的数据集中。
let scale = d3.scaleLinear().domain([1,5]).range([0,100])
映射关系:
值得注意的是,上述代码只是定义了一个映射规则,映射的输入值并不局限于 domain()
中的输入域。
scale(1) // 输出:0
scale(4) // 输出:75
scale(5) // 输出:100
scale(-1) // 输出:-50
scale(10) // 输出:225
于是我们来改造 3~4
的例子:
let dataset = [1,2,3,4,5];
let svgWidth = 500, svgHeight = 300, barPadding = 5;
let barWidth = (svgWidth / dataset.length);
let svg = d3.select('svg')
.attr("width", svgWidth)
.attr("height", svgHeight);
let yScale = d3.scaleLinear()
.domain([0, d3.max(dataset)])
.range([0, svgHeight]);
let barChart = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("y", d => svgHeight - yScale(d))
.attr("height", d => yScale(d))
.attr("width", barWidth - barPadding)
.attr("transform", (d, i) => {
let translate = [barWidth * i, 0];
return "translate(" translate ")";
});
然后就会得到以下图形:
6. Axes
:轴
轴是任何图表的组成部分,本例子中将会用到上面讲到的比例尺函数。
代码语言:javascript复制let data= [80, 100, 56, 120, 180, 30, 40, 120, 160];
let svgWidth = 500, svgHeight = 300;
let svg = d3.select('svg')
.attr("width", svgWidth)
.attr("height", svgHeight);
// 首先是拿最大值构建x轴坐标
let xScale = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([0, svgWidth]);
// 接下来是反转值,用作y轴坐标。
let yScale = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([svgHeight, 0]);
// 横轴的API使用
let x_axis = d3.axisBottom()
.scale(xScale);
// 纵轴的API使用
let y_axis = d3.axisLeft()
.scale(yScale);
// 在svg中提供了如g元素这样的将多个元素组织在一起的元素。
// 由g元素编组在一起的可以设置相同的颜色,可以进行坐标变换等,类似于Vue中的 <template>
svg.append("g")
.attr("transform", "translate(50, 10)")
.call(y_axis);
let xAxisTranslate = svgHeight - 20;
svg.append("g")
.attr("transform", "translate(50, " xAxisTranslate ")")
.call(x_axis);
7. 创建简易的 SVG
元素
在这里面,你会创建 <rect>
, <circle>
和 <line>
元素
let svgWidth = 600, svgHeight = 500;
let svg = d3.select("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.attr("class", "svg-container")
let line = svg.append("line")
.attr("x1", 100)
.attr("x2", 500)
.attr("y1", 50)
.attr("y2", 50)
.attr("stroke", "red");
let rect = svg.append("rect")
.attr("x", 100)
.attr("y", 100)
.attr("width", 200)
.attr("height", 100)
.attr("fill", "#9B95FF");
let circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 300)
.attr("r", 80)
.attr("fill", "#7CE8D5");
8. 创建饼图
代码语言:javascript复制let data = [
{"platform": "Android", "percentage": 40.11},
{"platform": "Windows", "percentage": 36.69},
{"platform": "iOS", "percentage": 13.06}
];
let svgWidth = 500, svgHeight = 300, radius = Math.min(svgWidth, svgHeight) / 2;
let svg = d3.select('svg')
.attr("width", svgWidth)
.attr("height", svgHeight);
//Create group element to hold pie chart
let g = svg.append("g")
.attr("transform", "translate(" radius "," radius ")") ;
// d3.scaleOrdinal() 序数比例尺
// schemeCategory10, 颜色比例尺
// D3提供了一些颜色比例尺,10就是10种颜色,20就是20种:
let color = d3.scaleOrdinal(d3.schemeCategory10);
let pie = d3.pie().value(d => d.percentage);
let path = d3.arc()
.outerRadius(radius)
.innerRadius(0);
let arc = g.selectAll("arc")
.data(pie(data))
.enter()
.append("g");
arc.append("path")
.attr("d", path)
.attr("fill", d => color(d.data.percentage));
let label = d3.arc()
.outerRadius(radius)
.innerRadius(0);
arc.append("text")
.attr("transform", d => `translate(${label.centroid(d)})`)
.attr("text-anchor", "middle")
.text(d => `${d.data.platform}:${d.data.percentage}%`);
9. 创建折线图
最后,你将学习如何创建折线图以显示近四个月的比特币价格。要获取数据,你将使用外部API。这个项目还将你在整个课程中学到的很多概念结合在一起,所以这是一个很好的可视化课程结束。
代码语言:javascript复制// 外部API,注意日期记得补零
const api = 'https://api.coindesk.com/v1/bpi/historical/close.json?start=2019-03-31&end=2019-07-01';
/**
* dom内容加载完毕时,从API中加载数据
*/
document.addEventListener("DOMContentLoaded", function(event) {
fetch(api)
.then(response => response.json())
.then(data => {
let parsedData = parseData(data);
drawChart(parsedData);
})
.catch(err => console.log(err))
});
/**
* 将数据解析为键值对
*/
parseData = data =>{
let arr = [];
for (let i in data.bpi) {
arr.push({
date: new Date(i), //date
value: data.bpi[i] //convert string to number
});
}
return arr;
}
/**
* 创建图表
*/
drawChart = data => {
let svgWidth = 600, svgHeight = 400;
let margin = { top: 20, right: 20, bottom: 30, left: 50 };
let width = svgWidth - margin.left - margin.right;
let height = svgHeight - margin.top - margin.bottom;
let svg = d3.select('svg')
.attr("width", svgWidth)
.attr("height", svgHeight);
let g = svg.append("g")
.attr("transform", "translate(" margin.left "," margin.top ")");
let x = d3.scaleTime()
.rangeRound([0, width]);
let y = d3.scaleLinear()
.rangeRound([height, 0]);
let line = d3.line()
.x(d=> x(d.date))
.y(d=> y(d.value))
x.domain(d3.extent(data, function(d) { return d.date }));
y.domain(d3.extent(data, function(d) { return d.value }));
g.append("g")
.attr("transform", "translate(0," height ")")
.call(d3.axisBottom(x))
.select(".domain")
.remove();
g.append("g")
.call(d3.axisLeft(y))
.append("text")
.attr("fill", "#000")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("text-anchor", "end")
.text("Price ($)");
g.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
}
以上原实例均来自:Learn D3 for free。scrimba是一个非常神奇的网站。它是使用交互式编码截屏工具构建的。
所有的操作都是:
暂停截屏视频 → 编辑代码 → 运行它!→ 查看更改
非常值得安利一波。接下来进入第二部分: Vue
中使用 D3.js
的正确姿势
2. Vue
中使用 D3.js
的正确姿势
我们将使用 D3
和 Vue
构建一个基本的柱状图组件。网上有一堆例子,但我们将专注于写 Vue
,而不是滥用D3。
1. 安装依赖
首先,我们需要为项目安装依赖项。我们可以简单地安装和使用 D3
整库:
npm i d3
但我在前面讲到,实际上 D3
是几个分库的集合,考虑到项目的优化,我们只安装所需的模块。
使用 VueCli
初始化项目即可。
2. 创建柱状图
3. 柱状图模块导入
4. 创建 svg
元素
因 Vue
数据响应的特性,我们不需要用到 D3
操作 DOM
的那套链式创建。
5. 数据与窗口大小响应
在 mounted
钩子中,我们将为窗口调整大小事件添加一个监听器,它将触发绘制动画,并将 <svg>
大小设置为新窗口的比例。我们不会立即渲染,而是等待 300
毫秒,以确保完全调整窗口大小。
以下是完整的 BarChart.vue
,请配合注释食用:
<template>
<div id="container" class="svg-container" align="center">
<h1>{{ title }}</h1>
<svg v-if="redrawToggle === true" :width="svgWidth" :height="svgHeight">
<g>
<rect
v-for="item in data"
class="bar-positive"
:key="item[xKey]"
:x="xScale(item[xKey])"
:y="yScale(0)"
:width="xScale.bandwidth()"
:height="0"
></rect>
</g>
</svg>
</div>
</template>
<script>
import { scaleLinear, scaleBand } from "d3-scale";
import { max, min } from "d3-array";
import { selectAll } from "d3-selection";
import { transition } from "d3-transition";
export default {
name: "BarChart",
props: {
title: String,
xKey: String,
yKey: String,
data: Array
},
mounted() {
this.svgWidth = document.getElementById("container").offsetWidth * 0.75;
this.AddResizeListener();
this.AnimateLoad();
},
data: () => ({
svgWidth: 0,
redrawToggle: true
}),
methods: {
// 绘制柱形
AnimateLoad() {
selectAll("rect")
.data(this.data)
.transition()
.delay((d, i) => {
return i * 150;
})
.duration(1000)
.attr("y", d => {
return this.yScale(d[this.yKey]);
})
.attr("height", d => {
return this.svgHeight - this.yScale(d[this.yKey]);
});
},
// 调整窗口大小后300毫秒重新绘制图表
// 即响应式绘制
AddResizeListener() {
window.addEventListener("resize", () => {
this.$data.redrawToggle = false;
setTimeout(() => {
this.$data.redrawToggle = true;
this.$data.svgWidth =
document.getElementById("container").offsetWidth * 0.75;
this.AnimateLoad();
}, 300);
});
}
},
computed: {
dataMax() {
return max(this.data, d => {
return d[this.yKey];
});
},
dataMin() {
return min(this.data, d => {
return d[this.yKey];
});
},
xScale() {
return scaleBand()
.rangeRound([0, this.svgWidth])
.padding(0.1)
.domain(
this.data.map(d => {
return d[this.xKey];
})
);
},
// 通过线性比例尺自动生成
yScale() {
return scaleLinear()
.rangeRound([this.svgHeight, 0])
.domain([this.dataMin > 0 ? 0 : this.dataMin, this.dataMax]);
},
svgHeight() {
return this.svgWidth / 1.61803398875; // 黄金比例
}
}
};
</script>
<style scoped>
.bar-positive {
fill: steelblue;
transition: r 0.2s ease-in-out;
}
.bar-positive:hover {
fill: brown;
}
.svg-container {
display: inline-block;
position: relative;
width: 100%;
padding-bottom: 1%;
vertical-align: top;
overflow: hidden;
}
</style>
我们将从父组件 App.vue
获取数据:
<template>
<div id="app">
<BarChart title="Bar Chart" xKey="name" yKey="amount" :data="barChartData"/>
</div>
</template>
<script>
import BarChart from "./components/BarChart.vue";
export default {
name: "App",
components: {
BarChart
},
data: () => ({
barChartData: [
{
name: "张三",
amount: 25
},
{
name: "李四",
amount: 40
},
{
name: "老王",
amount: 15
},
{
name: "老赖",
amount: 9
}
]
})
};
</script>
<style>
#app {
font-family: "Open Sans", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #282f36;
margin-top: 30px;
}
</style>
这时候 yarn run serve
后将会看到:
好像还缺点显示数值,考虑到该图高度是根据比例尺生成,我们调整下y坐标:
代码语言:javascript复制yScale() {
return scaleLinear()
.rangeRound([this.svgHeight, 0])
.domain([this.dataMin > 0 ? 0 : this.dataMin 2, this.dataMax 2]);
},
在 AnimateLoad()
末尾添加:
selectAll("text")
.data(this.data)
.enter()
最后在 <g>
元素中添加:
<text
v-for="item in data"
:key="item[xKey]"
:x="xScale(item[xKey]) 30"
:y="yScale(item[yKey]) - 2"
fill="red"
>{{ item[xKey]}} {{ item[yKey]}}
</text>
3. 参考文章
- D3 is not a Data Visualization Library
- D3中常用的比例尺
- D3 vs G2 vs Echarts
- Dynamic Data Visualizations With Vue.js and D3
4. 总结
该库几乎凭 MikeBostock
一人之力完成,且在学术界、专业团队中享有极大声誉。
D3
更接近底层,与g2
、echarts
不同,d3
能直接操作svg
,所以拥有极大的自由度,几乎可以实现任何 2d 的设计需求。- 正如其名
DataDrivenDocuments
,其本质是将数据与DOM
绑定,并将数据映射至DOM
属性上。 D3
长于可视化,而不止于可视化,还提供了数据处理
、数据分析
、DOM
操作等诸多功能。- 如果有想深耕数据可视化方面的前端,
D3
不得不学。
掌握 D3
后,限制作品水平的只会是想象力而不再是技术。
源码地址:点这里
作者掘金文章总集
需要转载到公众号的喊我加下白名单就行了。
- 「真®全栈之路」Web前端开发的后端指南
- 「Vue实践」5分钟撸一个Vue CLI 插件
- 「Vue实践」武装你的前端项目
- 「中高级前端面试」JavaScript手写代码无敌秘籍
- 「从源码中学习」面试官都不知道的Vue题目答案
- 「从源码中学习」Vue源码中的JS骚操作
- 「从源码中学习」彻底理解Vue选项Props
- 「Vue实践」项目升级vue-cli3的正确姿势
- 为何你始终理解不了JavaScript作用域链?