用D3.js實現動態條形圖(Bar chart race)

公司項目要求作動態條形圖,也就是Bar chart race,原本想從網上找個demo發覺沒有合適的,就本身寫了一個,完整代碼能夠去 個人GitHub 上查看。動態條形圖能夠很好地對比多個數據隨之間變化的趨勢。該demo是基於 d3v6 版本,因爲僅使用了一些最基礎的 api,因此在以前版本應該也能夠跑起來。本文章僅提供一種實現的思路,不對 api 和「進入、更新、退出」模式做詳細講解,因此在閱讀以前最後對它們有一個最基本的瞭解。
首先須要找一個合適的數據源,這裏我就從網上隨便找了一份。git

初始化變量github

const width = 1200, height = 600, margin = { top: 20, bottom: 0, left: 50, right: 80 };
const chartWidth = width - (margin.left + margin.right), chartHeight = height - (margin.top + margin.bottom);
const data = [];
const count = 10;
const duration = 500;
const barPadding = 20;
const barHeight = (chartHeight - (barPadding * count)) / count;
const getDate = () => dataOri[0][dateIndex];
let dateIndex = 1;
let date = getDate();
let dataSlice = [];
let chart = null, scale = null, axis = null, svg = null, dateTitle = null;

首先設定長、寬、外邊距,和圖表尺寸。data 存放格式化後的數據。因爲圖表不可能把數據源中全部的行都顯示出來,因此這裏只取前 10 個。每隔 10 秒切換一縱列。柱間距爲 20 。用圖表高度減去柱間距乘以柱數量再除以柱數量得每一個柱的寬。定義一個函數來獲取當前列表頭,這裏就是日期,賦給 date。定義 dataSlice 來存放當前日期下的全部數據。最後定義 chart 存放圖表實例,scale 存放比例尺,axis 存放座標軸,svg 存放畫布,dateTitle 存放當前列表頭。api

const createSvg = () => svg = d3.select('#chart').append('svg').attr('width', width).attr('height', height);

建立一個 svg 設置寬高並 append 到預先寫好的 container 中。app

格式化數據dom

function randomRgbColor() {
  const r = Math.floor(Math.random() * 256);
  const g = Math.floor(Math.random() * 256);
  const b = Math.floor(Math.random() * 256);
  return `rgb(${r},${g},${b})`;
}

先聲明一個建立隨機顏色的函數,用來給條形上色。
image.png
咱們的數據源是這種形式的,須要對其進行簡單的格式化,成爲一個一個條目svg

const formatData = () => {
  dataOri[0].forEach((date, index) => {
    if (index > 0) {
      dataOri.forEach((row, rowIndex) => {
        if (rowIndex > 0) {
          data.push({
            name: row[0],
            value: Number(row[index]),
            lastValue: index > 1 ? Number(row[index - 1]) : 0,
            date: date,
            color: randomRgbColor()
          });
         }
      });
    }
  });
}

兩層循環,第一層循環列,第二層循環行,存入行表頭,數據,上一列的數據,若是沒有就寫 0,列表頭,和一個隨機的顏色,用做給條形圖上色,至於 lastValue 的用途以後用到了會詳細解釋。
格式化後的數據如圖所示:
image.png函數

const sliceData = () =>
  dataSlice = data.filter(d => d.date === date).sort((a, b) => b.value - a.value).slice(0, count);

篩選出當天的數據,倒敘排列並取前 10 個。工具

建立座標軸動畫

const createScale = () =>
  scale = d3.scaleLinear().domain([0, d3.max(dataSlice, d => d.value)]).range([0, chartWidth]);

首先把比例尺建立出來,定義域是 0 到當天的最大值,值域是 0 到圖表寬度,對 d3.jsapi 不熟悉的同窗能夠去官網補習一下或者自行百度,經常使用的基本就那麼幾個。
座標軸的最終效果以下圖所示:
image.pngthis

須要對座標軸進行簡單的配置

const renderAxis = () => {
  createScale();
  axis = d3.axisTop().scale(scale).ticks(5).tickPadding(10).tickSize(0);
  svg.append('g')
    .classed('axis', true)
    .style('transform', `translate3d(${margin.left}px, ${margin.top}px, 0)`)
    .call(axis);
}

調用以前定義的比例尺函數建立比例尺,而後設置頂部的座標軸,ticks 設置 5 個刻度(這個方法比較有意思,雖然設置了5,可是不必定真的是 5,可能比 5 多也可能比 5 少),tickPadding 設置刻度與數值之間的間距,tickSize 設置刻度線長度,這裏不讓它顯示。設置完成以後 append 到圖表中,水平位移,讓出邊距的位置。

建立參考線
image.png
這條豎線就是參考線,從座標軸刻度延伸出來,貫穿整個圖表。

const renderAxisLine = () => {
  d3.selectAll('g.axis g.tick').select('line.grid-line').remove();
  d3.selectAll('g.axis g.tick').append('line')
    .classed('grid-line', true)
    .attr('stroke', 'black')
    .attr('x1', 0)
    .attr('y1', 0)
    .attr('x2', 0)
    .attr('y2', chartHeight);
}

因爲隨着數據的變化,參考線是不斷變化的,該函數會被反覆調用,因此要在一開始清除上一組數據的參考線。而後在座標軸每個有刻度線的位置都 append 一條線進去,x1y1 是該條線相對於父元素的左端點,x2y2 是右端點,因爲要貫穿整個圖表,因此右端點的 y 座標設置爲 chartHeight

建立列表頭
image.png
圖表右下鍵這個日期,也就是列的表頭

const renderDateTitle = () => {
  dateTitle = svg.append('text')
    .classed('date-title', true)
    .text(date)
    .attr('x', chartWidth - margin.top)
    .attr('y', chartHeight - margin.left)
    .attr('fill', 'rgb(128, 128, 128)')
    .attr('font-size', 40)
    .attr('text-anchor', 'end')
}

位移至右下角,設置顏色。這裏重點說一下 text-anchor,主要運用在 svg<text> 標籤的一個屬性,設置文本的對其方式,設置爲 end 表示文本字符串的末尾即當前文本的初始位置。

建立圖表主體

const createChart = () => {
  chart = svg.append('g')
    .classed('chart', true)
    .style('transform', `translate3d(${margin.left}px, ${margin.top}px, 0)`);
}

建立一個容器存放多個條形,並移到正中央。

const renderChart = () => {
  // 進入、更新、退出模式
}

該函數存放「進入、更新、退出」模式的代碼,因爲該模式能夠單獨引伸出一篇文章去講解,不太瞭解的同窗仍是建議先去自行理解一下。

const bars = chart.selectAll('g.bar').data(dataSlice, (d) => d.name);
let barsEnter;
barsEnter = bars.enter()
  .append('g')
  .classed('bar', true)
  .style('transform', (d, i) => `translate3d(0, ${calTranslateY(i)}px, 0)`);
dateIndex > 1 && barsEnter
  .transition().duration(this.duration)
  .style('transform', (d, i) => `translate3d(0, ${calTranslateY(i, 'end')}px, 0)`);
barsEnter.append('rect')
  .style('width', d => scale(d.value))
  .style('height', barHeight + 'px')
  .style('fill', d => d.color);
barsEnter.append('text')
  .classed('label', true)
  .text(d => d.name)
  .attr('x', '-5')
  .attr('y', barPadding)
  .attr('font-size', 14)
  .style('text-anchor', 'end');
barsEnter.append('text')
  .classed('value', true)
  .text(d => d.value)
  .attr('x', d => scale(d.value) + 10)
  .attr('y', barPadding);

將圖形與 dataSlice 綁定,barsEnter 表明的是綁定了數據的圖形,設置它的寬,高和顏色,條形左側的 y 軸,這裏對應的國家的名字,還有右側數值標註。這裏用到了一個工具函數:

const calTranslateY = (i, end) => {
  if (dateIndex === 1 || end) {
    return (barHeight + barPadding) * i + (barPadding / 2);
  } else {
    return (barHeight + barPadding) * (count + 1);
  }
}

當數據爲第一列或者傳入 end 的時候條形的縱軸位置在排序所在的位置,不然都放在圖表外面,等待進入。

bars.transition().duration(duration).ease(d3.easeLinear)
  .style('transform', function (d, i) {
    return 'translate3d(0, ' + calTranslateY(i, 'end') + 'px, 0)';
 })
  .select('rect')
  .style('width', function (d) {
    return scale(d.value) + 'px';
 });
bars
  .select('text.value')
  .transition().duration(duration).ease(d3.easeLinear)
  .attr('x', function (d) {
    return scale(d.value) + 10;
 })
  .tween('text', function (d) {
    const textDom = this;
    const i = d3.interpolateRound(d.lastValue, d.value);
    return (t) => textDom.textContent = i(t);
 });

更新模式,第一個方法鏈目的是條形按照順序排序,而且根據數值設定寬度。第一個方法鏈是設定右面標註的數值,而且自定義了一個數值過渡,讓數值的增加沒有那麼生硬,這裏用到了一開始格式化數據的時候設置的 lastValue

bars.exit()
  .transition().duration(duration).ease(d3.easeLinear)
  .style('transform', function (d, i) {
    return 'translate3d(0, ' + calTranslateY(i) + 'px, 0)';
 })
  .style('width', function (d) {
    return scale(d.value) + 'px';
 })
  .remove();

退出模式,將退出後的條形移到屏幕外並刪除。

調用方法

const init = () => {
  createSvg(); // 建立一個svg
  formatData(); // 格式化數據
  sliceData(); // 截取當天數據
  renderAxis(); // 渲染座標軸
  renderAxisLine(); // 渲染指示線
  renderDateTitle(); // 渲染日期
  createChart(); // 建立圖表
  renderChart(); // 渲染圖表
  createTicker(); // 建立定時器
}
init();

依次調用一開始聲明的那些方法,還有最後一個 createTicker 方法沒有聲明

function createTicker() {
  const ticker = d3.interval(() => {
    if (dateIndex < dataOri[0].length - 1) {
      dateIndex++;
      date = getDate();
      dateTitle.text(date);
      sliceData();
      updateAxis();
      renderAxisLine();
      renderChart();
    } else {
      ticker.stop();
    }
  }, duration);
}

建立了一個定時器,每隔 duration 設定的事件進行切換,更新座標軸、輔助線、圖表等,這裏用到了 updateAxis 方法。

const updateAxis = () => {
  createScale();
  axis.scale().domain([0, d3.max(dataSlice, d => d.value)]);
  svg.select('g.axis')
    .transition().duration(duration).ease(d3.easeLinear)
    .call(axis);
  d3.selectAll('g.axis g.tick text').attr('font-size', 14);
}

該方法用於當數據改變時更新座標軸。

總結
動態條形圖的全部功能都已經開發完了,打開頁面就能夠看到動畫效果了。完整代碼能夠個人 GitHub 中下載。其實該圖表算是 d3 比較入門的效果,掌握了「進入、更新、退出」模式和過渡以後就能夠開發出來了。本文提供的思路也並不是該圖表實現的最優解,若有更好的實現方法歡迎留言討論。

相關文章
相關標籤/搜索