「数据可视化库王者」D3.js 极速上手到Vue应用

640?wx_fmt=gif戳蓝字「高级前端进阶」关注我们哦!


前言

D3近年来一直是 JavaScript最重要的数据可视化库之一,在创建者 MikeBostock的维护下,前景依然无量,至少现在没有能打的:

  • D3与众多其他库的区别在于无限定制的能力(直接操作 SVG)。

  • 它的底层 API提供对原生 SVG元素的直接控制,但它也带来了高学习曲线的成本。

  • 我们将把 D3和 Vue结合在一起 - 使用 Vue的动态数据绑定,清晰的语法和模块化结构,可以充分发挥 D3的最佳性能。 640?wx_fmt=png根据广泛定义,D3可拆分为以下几种分库:

640?wx_fmt=png

  1. 绝大部分的 D3课程或书籍,都会着重讲解在其 DOM操作功能上,但这明显与近几年来的web框架理念相违背。

  2. 用于数据可视化的 D3,其核心在于使用绘图指令装饰数据,从源数据创建新的可绘制数据,生成 SVG路径以及从数据和方法在 DOM中创建数据可视化元素(如轴)的功能。 640?wx_fmt=png

  3. 有许多用于管理DOM的工具,所有这些工具都可以在 D3中集成数据可视化功能。这也是 D3能与 Vue无缝结合的原因之一。

于此,我们不需要从 D3 DOM操作功能开始学起,直接通过实例来入门 D3

D3.js 渐进入门

以下实例的模版均为以下形式:

<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. 选择和操作

640?wx_fmt=png

你需要学习的第一件事是如何使用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.数据加载和绑定

640?wx_fmt=png当你要创建可视化时,了解如何加载数据以及将其绑定到DOM非常重要。所以在这个实例中,你将学到这两点。

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.创建一个简单的柱状图

640?wx_fmt=png首先需要添加一个 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. 在图形上方显示数值

640?wx_fmt=png

这时就需要在上述代码中创建 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])

映射关系:

640?wx_fmt=png

值得注意的是,上述代码只是定义了一个映射规则,映射的输入值并不局限于 domain()中的输入域。

scale(1) // 输出:0	
scale(4) // 输出:75	
scale(5) // 输出:100	
scale(-1) // 输出:-50	
scale(10) // 输出:225

于是我们来改造 34的例子:

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 +")";	
    });

然后就会得到以下图形:

640?wx_fmt=png

6. Axes:轴

640?wx_fmt=png

轴是任何图表的组成部分,本例子中将会用到上面讲到的比例尺函数。

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元素

640?wx_fmt=png在这里面,你会创建 <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. 创建饼图

640?wx_fmt=png

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. 创建折线图

640?wx_fmt=png

最后,你将学习如何创建折线图以显示近四个月的比特币价格。要获取数据,你将使用外部API。这个项目还将你在整个课程中学到的很多概念结合在一起,所以这是一个很好的可视化课程结束。

// 外部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是一个非常神奇的网站。它是使用交互式编码截屏工具构建的。

640?wx_fmt=gif

所有的操作都是:

暂停截屏视频 → 编辑代码 → 运行它!→ 查看更改

非常值得安利一波。接下来进入第二部分: Vue中使用 D3.js的正确姿势

2. Vue中使用 D3.js的正确姿势

我们将使用 D3和 Vue构建一个基本的柱状图组件。网上有一堆例子,但我们将专注于写 Vue,而不是滥用D3。

1. 安装依赖

首先,我们需要为项目安装依赖项。我们可以简单地安装和使用 D3整库:

npm i d3

但我在前面讲到,实际上 D3是几个分库的集合,考虑到项目的优化,我们只安装所需的模块。

640?wx_fmt=png

使用 VueCli 初始化项目即可。

2. 创建柱状图

640?wx_fmt=png

3. 柱状图模块导入

640?wx_fmt=png

4. 创建 svg元素

640?wx_fmt=png

因 Vue数据响应的特性,我们不需要用到 D3操作 DOM的那套链式创建。

5. 数据与窗口大小响应

640?wx_fmt=png在 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后将会看到:

640?wx_fmt=gif

好像还缺点显示数值,考虑到该图高度是根据比例尺生成,我们调整下y坐标:

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>

640?wx_fmt=png

3. 参考文章

    

640?wx_fmt=gif

  • D3 is not a Data Visualization Library

  • D3中常用的比例尺

  • D3 vs G2 vs Echarts

  • Dynamic Data Visualizations With Vue.js and D3

4. 总结

该库几乎凭 MikeBostock 一人之力完成,且在学术界、专业团队中享有极大声誉。

640?wx_fmt=png

  • D3更接近底层,与 g2、 echarts 不同, d3 能直接操作 svg,所以拥有极大的自由度,几乎可以实现任何 2d 的设计需求。

  • 正如其名 DataDrivenDocuments,其本质是将数据与 DOM 绑定,并将数据映射至 DOM 属性上。

  • D3 长于可视化,而不止于可视化,还提供了 数据处理、 数据分析、 DOM 操作等诸多功能。

  • 如果有想深耕数据可视化方面的前端, D3不得不学。

640?wx_fmt=png

掌握 D3 后,限制作品水平的只会是想象力而不再是技术。

源码地址:点这里

作者掘金文章总集

需要转载到公众号的喊我加下白名单就行了。

  • 「真®全栈之路」Web前端开发的后端指南

  • 「Vue实践」5分钟撸一个Vue CLI 插件

  • 「Vue实践」武装你的前端项目

  • 「中高级前端面试」JavaScript手写代码无敌秘籍

  • 「从源码中学习」面试官都不知道的Vue题目答案

  • 「从源码中学习」Vue源码中的JS骚操作

  • 「从源码中学习」彻底理解Vue选项Props

  • 「Vue实践」项目升级vue-cli3的正确姿势

  • 为何你始终理解不了JavaScript作用域链?


❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-

  2. 关注我的 GitHub:github.com/yygmind,让我们成为长期关系

  3. 关注公众号「高级前端进阶」,每周重点攻克一个前端面试重难点,公众号后台回复「面试题」 送你高级前端面试题。

640?wx_fmt=png

展开阅读全文

没有更多推荐了,返回首页