「數據可視化庫王者」D3.js 極速上手到Vue應用

前言

D3近年來一直是JavaScript最重要的數據可視化庫之一,在建立者Mike Bostock的維護下,前景依然無量,至少如今沒有能打的:css

  • D3與衆多其餘庫的區別在於無限定製的能力(直接操做SVG)。
  • 它的底層API提供對原生SVG元素的直接控制,但它也帶來了高學習曲線的成本。
  • 咱們將把D3Vue結合在一塊兒 - 使用Vue的動態數據綁定,清晰的語法和模塊化結構,能夠充分發揮D3的最佳性能。

根據普遍定義,D3可拆分爲如下幾種分庫:html

  1. 絕大部分的D3課程或書籍,都會着重講解在其DOM操做功能上,但這明顯與近幾年來的web框架理念相違背。
  2. 用於數據可視化的D3,其核心在於使用繪圖指令裝飾數據,從源數據建立新的可繪製數據,生成SVG路徑以及從數據和方法在DOM中建立數據可視化元素(如軸)的功能。

3. 有許多用於管理DOM的工具,全部這些工具均可以在D3中集成數據可視化功能。這也是D3能與Vue無縫結合的緣由之一。前端

於此,咱們不須要從D3 DOM操做功能開始學起,直接經過實例來入門D3vue

1. D3.js 漸進入門

如下實例的模版均爲如下形式:git

<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的替代品。如下代碼請逐行添加運行。github

// 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很是重要。因此在這個實例中,你將學到這兩點。web

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中添加(已添加關鍵註釋):vue-cli

// 數據集
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. 在圖形上方顯示數值

這時就須要在上述代碼中建立svgtext文本npm

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:軸

軸是任何圖表的組成部分,本例子中將會用到上面講到的比例尺函數。

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. 建立餅圖

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。這個項目還將你在整個課程中學到的不少概念結合在一塊兒,因此這是一個很好的可視化課程結束。

// 外部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的正確姿式

咱們將使用D3Vue構建一個基本的柱狀圖組件。網上有一堆例子,但咱們將專一於寫Vue,而不是濫用D3

1. 安裝依賴

首先,咱們須要爲項目安裝依賴項。咱們能夠簡單地安裝和使用D3整庫:

npm i d3
複製代碼

但我在前面講到,實際上D3是幾個分庫的集合,考慮到項目的優化,咱們只安裝所需的模塊。

使用Vue Cli 初始化項目便可。

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座標:

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].amount"
  :x="xScale(item[xKey]) + 30"
  :y="yScale(item[yKey]) - 2"
  fill="red"
>{{ item[xKey]}} {{ item[yKey]}}
</text>
複製代碼

3. 參考文章

4. 總結

該庫幾乎憑 Mike Bostock 一人之力完成,且在學術界、專業團隊中享有極大聲譽。

  • D3更接近底層,與 g2echarts 不一樣,d3 能直接操做 svg,因此擁有極大的自由度,幾乎能夠實現任何 2d 的設計需求。
  • 正如其名 Data Driven Documents,其本質是將數據與 DOM 綁定,並將數據映射至 DOM 屬性上。
  • D3 長於可視化,而不止於可視化,還提供了數據處理數據分析DOM 操做等諸多功能。
  • 若是有想深耕數據可視化方面的前端,D3不得不學。

掌握 D3 後,限制做品水平的只會是想象力而再也不是技術。

源碼地址:點這裏

❤️ 看完三件事

若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)
  2. 關注公衆號「前端勸退師」,不按期分享原創知識。
  3. 也看看其它文章

](juejin.cn/post/684490…)

相關文章
相關標籤/搜索