D3 入门笔记
1.D3是什么
DOM
上,然后根据数据来计算对应 DOM
的属性值。我觉得,其设计思路就是把数据映射到视觉元素(形状、长度、方向、角度、颜色、面积、文本等),并通过视觉元素的组合进行数据可视化。下面的实例使用SVG来展示。- 使用外部链接
<script src="https://d3js.org/d3.v5.min.js" />
- 本地引入
<script src="./d3.v5.min.js" /> //src属性里写d3.js本地路径
- npm、yarnan安装依赖包
npm i d3 / yarn add d3
import * as d3 from "d3";
3.D3对象、选择集(d3-selection)、操作
- 引入d3.js后,就可以获得要使用的d3对象了。每次打点调用 d3 提供的函数方法,都会返回一个 d3 对象,这样可以继续调用 d3 的其他函数方法。
创建画布,绘制图形(Draw canvas)
1 // Create chart dimensions 2 // 定义svg图形宽高,以及柱状图间距 3 const svgWidth = 500, svgHeight = 300, barPadding = 14; 4 const margin = { 5 top: 20, 6 right: 40, 7 bottom: 20, 8 left: 40 9 } 10 11 // Draw canvas 12 // 绘制图形 13 const svg = d3.select('.d3-demo') 14 .attr('width', svgWidth) 15 .attr('height', svgHeight);
- D3 选择元素提供了两个函数:select 和 selectAll。
select 选中符合条件的第一个元素。
selectAll 选择所有与指定的 selector 匹配的元素。
详情:https://www.d3js.org.cn/document/d3-selection/#installing
- 选择元素后可以操作元素,操作元素主要分为增加 ( append )、插入 ( insert )、删除元素 ( remove ) 和设置获取元素属性 ( attr, style )。
1 console.log(d3.select('.d3-demo3')); 2 d3.select('.d3-demo3') 3 .style('background-color', '#c3272b') 4 .append('text') 5 .text('D3-demo-1') 6 .style('color', 'red') 7 .attr('x', 20) 8 .attr('y', 20) 9
运行结果:
4. 数据绑定
数据绑定是将数据绑定到 DOM 元素上,D3 中数据绑定提供了 datum() 和data()两个函数,一般都使用data()函数。
1 d3.select("body") 2 .selectAll("p") 3 .data([4, 8, 15, 16, 23, 42]) 4 .enter() 5 .append("p") 6 .text(function(d) { return "I’m number " + d + "!"; });
运行结果:
enter和exit操作
数据绑定的时候可能出现 DOM
元素与数据元素个数不匹配的问题,那么 enter
和 exit
就是用来处理这个问题的。enter
操作用来添加新的 DOM
元素,exit
操作用来移除多余的 DOM
元素。
如果数据元素多于 DOM
个数时用 enter
,如果数据元素少于 DOM
元素,则用 exit
。
在数据绑定时候存在三种情形:
- 数据元素个数多于
DOM
元素个数 - 数据元素与
DOM
元素个数一样 - 数据元素个数少于
DOM
元素个数
情形 1:数据元素个数多于 DOM
元素个数
如以下例子,如果文档中p
标签的个数少于数组个数(6个),则使用 enter
和 append
操作来补齐 DOM
元素:
d3.select("body") .selectAll("p") .data([4, 8, 15, 16, 23, 42]) .enter().append("p") .text(function(d) { return "I’m number " + d + "!"; });
情形 2:数据元素与 DOM
元素个数一样
如果这种情况再使用 data
来绑定数据,相当于是更新了每个 DOM
元素所对应的数据,此时不需要加入新节点也不需要删除多余的节点:
//update let p = d3.select("body") .selectAll("p") .data([4, 8, 15, 16, 23, 42]) .text(function(d) { return d; });
情形 3:数据元素个数少于 DOM
元素个数
假设p
元素的个数多于 6 个,数据元素个数为 6,则:
let p = d3.select("body") .selectAll("p") .data([4, 8, 15, 16, 23, 42]) .text(function(d) { return d; }); p.exit().remove() //移除多余的元素
但是在实际应用中,不可能先去统计一下元素个数,因此这三种情形可以一起使用:(在画图表时一般都是直接使用append添加元素,因为画布是从空白开始添加元素的)
// Update 情形2 let p = d3.select("body") .selectAll("p") .data([4, 8, 15, 16, 23, 42]) .text(function(d) { return d; }); // Enter 情形1 p.enter().append("p") .text(function(d) { return d; }); // Exit 情形3 p.exit().remove();
5. 过渡(d3-transition)
D3
支持动画效果,这种动画效果可以通过对样式属性的过渡实现。
比如对元素的背景颜色进行过渡:
d3.select("body").transition()
.style("background-color", "black");
详情:https://www.d3js.org.cn/document/d3-transition/#installing
https://observablehq.com/@d3/learn-d3-animation?collection=@d3/learn-d3
6. 比例尺(d3-scale)
比例尺是 D3 重要概念。
对于可视化来说,比例尺是一个很方便的工具:将抽象的维度数据映射为可视化表示。比例尺这个概念跟地图比例尺的概念是一样的,要一个画布上绘制一幅世界地图,需要按一定的比例将整个世界投射到画布上。
虽然经常使用位置编码定量数据,比如将测量单位米使用像素映射。但是比例尺可以对任何视觉编码进行映射,比如颜色,描边的宽度或者符号的大小。比例尺也可以用来对任意类型的数据进行映射,比如分类数据或离散的数据。
三个要素:定义域、值域、对应法则。
其中 scaleLinear() 对应法则,domain() 是定义域,range() 是值域。
以线性比例尺为例:
let x = d3.scaleLinear() .domain([10, 130]) .range([0, 960]); x(20); // 80 x(50); // 320
以线性比例尺为例(数值映射为颜色):
let color = d3.scaleLinear() .domain([10, 100]) .range(["brown", "steelblue"]); color(20); // "#9a3439" color(50); // "#7b5167"
详情:https://www.d3js.org.cn/document/d3-scale/#installing
7. 坐标轴(d3-axis)
坐标轴组件可以将 scales (opens new window)显示为人类友好的刻度标尺参考,减轻了在可视化中的视觉任务。
例:
// Create chart dimensions
// 定义svg图形宽高,以及柱状图间距
const svgWidth = 500, svgHeight = 300, barPadding = 14;
const margin = {
top: 20,
right: 40,
bottom: 20,
left: 40
}
// Draw canvas
// 绘制图形
const svg = d3.select('.d3-demo')
.attr('width', svgWidth)
.attr('height', svgHeight);
// Create scales
// 首先是拿到最大值构建x轴坐标
let xScale = d3.scaleLinear()
.domain([0, d3.max(dataX)])
.range([margin.left, svgWidth - margin.right]);
// 接下来是反转值,用作y轴坐标
let yScale = d3.scaleLinear()
.domain([0, d3.max(dataY) + 20])
.range([svgHeight - margin.bottom, margin.top])
// 横轴的API使用
let x_axis = d3.axisBottom()
.scale(xScale)
// 纵轴的API使用
let y_axis = d3.axisLeft()
.scale(yScale);
svg.append("g")
.attr("transform", `translate(${0}, ${svgHeight - margin.bottom})`)
.call(x_axis);
svg.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(y_axis);
运行结果:
8. 基本图表
数据来源:使d3-fetch模块中的d3.json()读取json文件的数据
{
"code": 200,
"message": "操作成功",
"data": [
{
"name": 0,
"value": 20
},
{
"name": 1,
"value": 40
},
{
"name": 2,
"value": 100
},
{
"name": 3,
"value": 56
},
{
"name": 4,
"value": 120
},
{
"name": 5,
"value": 180
},
{
"name": 6,
"value": 30
},
{
"name": 7,
"value": 40
},
{
"name": 8,
"value": 40
}
]
}
- 折线图
<template> <div id="d3Demo"> <svg class="d3-demo"></svg> </div> </template> <script lang="ts"> import * as d3 from "d3"; import { defineComponent, reactive, ref, toRefs, h, watch, nextTick } from "vue"; export default defineComponent({ name: "D3Line", setup(props, context) { let dataList = []; let dataX = []; let dataY = []; // 获取数据 async function getData() { const xyData = await d3.json('/dataFiles/demo1.json'); let { data: datas } = xyData; dataList = datas; datas.forEach(element => { dataX.push(element.name); dataY.push(element.value); }); } // D3相关函数 const d3Function = async () =>{ // Access data await getData(); // Create chart dimensions // 定义svg图形宽高,以及柱状图间距 const svgWidth = 500, svgHeight = 300, barPadding = 14; const margin = { top: 20, right: 40, bottom: 20, left: 40 } // Draw canvas // 绘制图形 const svg = d3.select('.d3-demo') .attr('width', svgWidth) .attr('height', svgHeight); // Create scales // 首先是拿到最大值构建x轴坐标 let xScale = d3.scaleLinear() .domain([0, d3.max(dataX)]) .range([margin.left, svgWidth - margin.right]); // 接下来是反转值,用作y轴坐标 let yScale = d3.scaleLinear() .domain([0, d3.max(dataY) + 20]) .range([svgHeight - margin.bottom, margin.top]) // 横轴的API使用 let x_axis = d3.axisBottom() .scale(xScale) // 纵轴的API使用 let y_axis = d3.axisLeft() .scale(yScale); // Draw data const t = (path) => path.transition() .duration(1000) .ease(d3.easeLinear) .attrTween('stroke-dasharray', function() { const length = this.getTotalLength(); return d3.interpolate(`0, ${length}`, `${length}, ${length}`); }) let line = d3.line() .x(function(d) { return xScale(d.name); }) .y(function(d) { return yScale(d.value); }) .curve(d3.curveCatmullRom.alpha(0.5)); // Draw peripherals // 在svg中提供了如g元素这样的将多个元素组织在一起的元素 // 由g元素编组在一起的可以设置相同的颜色,可以进行坐标变化等,类似于Vue中的<template> svg.append("path") // .attr("transform", `translate(${0}, ${svgHeight - margin.bottom})`) .attr('d', line(dataList)) .attr('class', 'line-path') .attr('fill', 'none') .attr('stroke-dasharray', '0,1') .attr('stroke', '#c3272b') .attr('stroke-width', 2) .call(t) svg.append("g") .attr("transform", `translate(${0}, ${svgHeight - margin.bottom})`) .call(x_axis); svg.append("g") .attr("transform", `translate(${margin.left}, 0)`) .call(y_axis); } nextTick(() => { d3Function(); }) }, }); </script> <style scoped lang="scss"> #d3Demo { width: 100%; height: 100%; color: $test-color; display: flex; align-items: center; background-size: 100% 100%; justify-content: center; } </style>
运行结果:
- 柱状图
<template> <div id="d3Demo"> <svg class="d3-demo"></svg> </div> </template> <script lang="ts"> import * as d3 from "d3"; import { defineComponent, reactive, ref, toRefs, h, watch, nextTick } from "vue"; export default defineComponent({ name: "D3Line", setup(props, context) { let dataList = []; let dataX = []; let dataY = []; // 获取数据 async function getData() { const xyData = await d3.json('/dataFiles/demo1.json'); let { data: datas } = xyData; dataList = datas; datas.forEach(element => { dataX.push(element.name); dataY.push(element.value); }); } // D3相关函数 const d3Function = async () =>{ // Access data await getData(); // Create chart dimensions // 定义svg图形宽高,以及柱状图间距 const svgWidth = 500, svgHeight = 300, barPadding = 7; const durationTime = 1000; // 动画时长 const margin = { top: 20, right: 40, bottom: 20, left: 40 } // 通过图形计算每个柱状宽度 let barWidth = (svgWidth - margin.left - margin.right) / dataX.length; // Draw canvas // 绘制图形 const svg = d3.select('.d3-demo') .attr('width', svgWidth) .attr('height', svgHeight); // Create scales // 首先是拿到最大值构建x轴坐标 let xScale = d3.scaleBand() .domain((dataX)) .range([margin.left, svgWidth - margin.right]) // 接下来是反转值,用作y轴坐标 let yScale = d3.scaleLinear() .domain([0, d3.max(dataY) + 20]) .range([svgHeight - margin.bottom, margin.top]) // y轴值得缩放 let yDataScale = d3.scaleLinear() .domain([0, d3.max(dataY) + 20]) .range([0, svgHeight - margin.top - margin.bottom]) // 横轴的API使用 let x_axis = d3.axisBottom() .scale(xScale) // 纵轴的API使用 let y_axis = d3.axisLeft() .scale(yScale); // Draw data const t = (val) => val.transition() .duration(durationTime) .ease(d3.easeLinear) .attrTween('height', function(d) { return d3.interpolate('0', yDataScale(d)); }) // const t1 = (val) => val.transition() .duration(durationTime) .ease(d3.easeLinear) .attrTween('transform', function(d, i) { return d3.interpolate(`translate(${barWidth * i + margin.left + barPadding}, ${(svgHeight - margin.bottom)})`, `translate(${barWidth * i + margin.left + barPadding}, ${(svgHeight - margin.bottom) - yDataScale(d)})`); }) // 处理文字动画 const t2 = (val) => val.transition() .duration(durationTime) // .delay(1000) .ease(d3.easeLinear) .textTween((d) => { return d3.interpolateRound(0, d); }) // Draw peripherals // 在svg中提供了如g元素这样的将多个元素组织在一起的元素 // 由g元素编组在一起的可以设置相同的颜色,可以进行坐标变化等,类似于Vue中的<template> let bar = svg.selectAll(".bar") .data(dataY) // .enter() // .append("g") .join("g") .attr("class", "bar") .call(t1) bar.append('rect') // 添加足够数量的矩形 .attr('class', 'rect') .attr("width", barWidth - barPadding * 2) // 设定宽度 .attr("height", d => yDataScale(d)) // 设定高度 .style("fill", '#c3272b') .each(function() { d3.select(this) .call(t); }) bar.append('text') .attr("transform", (d, i) => { let translate = [barWidth / 2 - barPadding, -2]; return `translate(${translate})`; }) .attr("text-anchor", "middle") .style("fill", '#000000') .each(function() { d3.select(this) .call(t2); }) svg.append("g") .attr("transform", `translate(${0}, ${svgHeight - margin.bottom})`) .call(x_axis); svg.append("g") .attr("transform", `translate(${margin.left}, 0)`) .call(y_axis); } nextTick(() => { d3Function(); }) }, }); </script> <style scoped lang="scss"> #d3Demo { width: 100%; height: 100%; color: $test-color; display: flex; align-items: center; background-size: 100% 100%; justify-content: center; } </style>
运行结果:
9. 小结
这篇文章只是介绍了d3的基本操作,官网有很多的知识点需要自行查看。当你看完这篇入门笔记后,能对d3有一个简单的认识,加上对SVG基本图形的了解可以创造出一些基础的图表,加油,加油!!!
最后给一个d3画图表的基本步骤,按这个步骤可以指导你画图思路。上面的实例就是按这个步骤实现的,可以作为参考。
Demo项目开源地址:https://gitee.com/userzp/vue3-d3.git