公司項目要求作動態條形圖,也就是Bar chart race,原本想從網上找個demo發覺沒有合適的,就本身寫了一個,完整代碼能夠去 個人GitHub 上查看。動態條形圖能夠很好地對比多個數據隨之間變化的趨勢。該demo是基於 d3
的 v6
版本,因爲僅使用了一些最基礎的 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})`; }
先聲明一個建立隨機顏色的函數,用來給條形上色。
咱們的數據源是這種形式的,須要對其進行簡單的格式化,成爲一個一個條目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
的用途以後用到了會詳細解釋。
格式化後的數據如圖所示:
函數
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.js
的 api
不熟悉的同窗能夠去官網補習一下或者自行百度,經常使用的基本就那麼幾個。
座標軸的最終效果以下圖所示:
this
須要對座標軸進行簡單的配置
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
到圖表中,水平位移,讓出邊距的位置。
建立參考線
這條豎線就是參考線,從座標軸刻度延伸出來,貫穿整個圖表。
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
一條線進去,x1
和 y1
是該條線相對於父元素的左端點,x2
和 y2
是右端點,因爲要貫穿整個圖表,因此右端點的 y
座標設置爲 chartHeight
。
建立列表頭
圖表右下鍵這個日期,也就是列的表頭
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
比較入門的效果,掌握了「進入、更新、退出」模式和過渡以後就能夠開發出來了。本文提供的思路也並不是該圖表實現的最優解,若有更好的實現方法歡迎留言討論。