D3.js 教程: 使用 JavaScript 建立可互動的柱狀圖

OFED發表於2019-03-02

原文連結:D3.js Tutorial: Building Interactive Bar Charts with JavaScript

譯者:OFED

最近,我們有幸參與了一個機器學習專案,該專案涉及 React 和 D3.js 之類的庫。在許多工中,我開發了幾個圖表用來展示諸如樸素貝葉斯這樣的機器學習模型的處理結果,圖表以折線圖或分組柱狀圖的形式呈現。

我會在此文中介紹使用 D3.js 的過程,以及通過一個簡單的柱狀圖示例演示庫的基本使用。

讀完此文後,你將學到如何輕鬆建立類似的 D3.js 圖表:

bar chart

這裡有完整的原始碼

我們在 RisingStack(公司)喜歡 JavaScript 生態,前端,後端都喜歡。就我個人而言,我對前後端都感興趣。通過後端開發,我可以看透應用程式的底層業務邏輯,同時也有機會在前端建立令人驚歎的效果。這正是 D3.js 的用武之地!

D3.js 是什麼?

D3.js 是一個可以基於資料來操作文件的 JavaScript 庫。

“D3 可以幫助你使用 HTML, CSS, SVG 以及 Canvas 來展示資料。D3 遵循現有的 Web 標準,可以不需要其他任何框架獨立執行在現代瀏覽器中,它結合強大的視覺化元件來驅動 DOM 操作。” – d3js.org

首先考慮為什麼要用 D3.js 建立圖表?為什麼不只顯示圖片呢?

圖表是基於第三方資源的資訊,在渲染時需要動態視覺化。此外,SVG 是一個非常強大的工具,非常適合這個應用場景。

讓我們先看看 SVG 有什麼好。

SVG 的優點

SVG 代表可縮放向量圖形,從技術上講,這是一種基於 XML 的標記語言。

它通常用於繪製向量圖形,比如線條和形狀或修改現有影像。你可以在這裡找到可用元素的列表。

優點:

  • 支援所有主流瀏覽器;
  • 有 DOM 介面,不需要第三方庫;
  • 可伸縮,可保持高解析度;
  • 和其他影像格式相比,體積更小。

缺點:

  • 只能顯示二維影像;
  • 學習曲線長;
  • 對於計算密集型操作,渲染可能需要很長時間。

SVG 儘管有缺點,但它仍是顯示圖示,logo,插圖或者此文提及的圖表的優良工具。

開始使用 D3.js

我選擇以柱狀圖作為開始,因為它代表了一個低複雜度的視覺元素,同時它還能教會 D3.js 本身的基本應用。沒騙你,D3 提供了一套很棒的視覺化資料的工具。看看它的 github page 頁面,欣賞一些非常好的用例!

柱狀圖可以是水平或垂直的,取決於它的方向。我們從垂直的柱狀圖開始。

在這個圖表中,我將根據 Stack Overlow 2018年開發者調查結果顯示前十個最受歡迎的程式語言。

畫起來!

SVG 的座標系從左上角開始(0;0)。正 x 軸向右,正 y 軸向下。因此,在計算元素的 y 座標時,必須考慮 SVG 的高度。

axis

背景知識差不多了,讓我們擼程式碼吧!

我想建立一個寬1000畫素、高600畫素的圖表。

<body>
    <svg />
</body>
<script>
    const margin = 60;
    const width = 1000 - 2 * margin;
    const height = 600 - 2 * margin;

    const svg = d3.select(`svg`);
</script>
複製程式碼

以上程式碼片段中,我用 d3 select 選擇了 HTML 建立的 <svg> 元素。此選擇方法接收各種型別的選擇器字串並返回第一個匹配元素。如果想獲取所有匹配元素,使用 selectAll

我還定義了一個邊距值,它給圖表提供了一點間距。間距也可以應用到 <g> 元素上,通過 translate 移動期望的值。從現在起,我將在這個分組中繪製,確保與頁面其它內容保持合理的間距。

const chart = svg.append(`g`)
    .attr(`transform`, `translate(${margin}, ${margin})`);
複製程式碼

往元素新增屬性就像呼叫 attr 方法一樣簡單。方法的第一個引數接收用於所選 DOM 元素的屬性。第二個引數是屬性值或返回其值的回撥函式。以上程式碼簡單將圖表的原點移到 SVG 的 (60;60) 位置。

D3.js 支援的資料來源格式

要開始繪圖,我需要定義使用的資料來源。本教程中,我使用了一個簡單的 JavaScript 陣列,該陣列儲存了語言名稱及其所佔百分比率的物件,但是這裡著重提到一點,D3.js 支援多種資料格式。

該庫具備從 XMLHttpRequest,.csv 檔案,文字檔案等資料來源載入資料的內建功能。每一種資料來源都可能包含 D3.js 可用的資料,最重要的是把它們構建成陣列。注意,從 版本5.0 開始,D3 庫使用 Promise 取代回撥來載入資料,這是一次不向後相容的更改。

縮放,座標軸

讓我們繼續討論圖表的座標軸。為了畫 y 軸,我需要設定最小和最大值,分別設定為0和100。

本教程中,我正在研究使用百分比,但是除了數字之外,還有其他資料型別的實用函式,我將在後面解釋。

我必須將圖表的高度在這兩個值之間均分。為此,我建立了一個縮放函式。

const yScale = d3.scaleLinear()
    .range([height, 0])
    .domain([0, 100]);
複製程式碼

線性縮放是最常見的縮放型別。它將連續輸入範圍轉換為連續輸出範圍。請注意 rangedomain 方法。第一個 range 方法取的長度應該在 domain 的邊界值之間。

記住,SVG 座標系從左上角開始,這就是為什麼 range 將高度作為第一個引數而不是零。

在左側建立一個座標軸跟新增另一個分組一樣簡單,呼叫 d3 的 axisLeft 方法,並把縮放函式作為引數。

chart.append(`g`)
    .call(d3.axisLeft(yScale));
複製程式碼

現在,繼續新增 x 軸。

const xScale = d3.scaleBand()
    .range([0, width])
    .domain(sample.map((s) => s.language))
    .padding(0.2)

chart.append(`g`)
    .attr(`transform`, `translate(0, ${height})`)
    .call(d3.axisBottom(xScale));
複製程式碼
D3.js 教程: 使用 JavaScript 建立可互動的柱狀圖

請注意,我使用 scaleBand 方法建立 x 軸,它將 x 軸 分成多段,並且使用餘下的間隙計算柱狀圖的座標和寬度。

D3.js 還能處理許多其他日期型別。scaleTimescaleLinear 非常相似,只是這裡的 domain 是一個日期陣列。

使用 D3.js 繪製柱狀圖

想想我們需要什麼樣的輸入來畫柱條。它們各自代表一個用簡單形狀,特別是矩形來展示的值。下一段程式碼中,我把它們新增到已建立的分組元素中了。

chart.selectAll()
    .data(goals)
    .enter()
    .append(`rect`)
    .attr(`x`, (s) => xScale(s.language))
    .attr(`y`, (s) => yScale(s.value))
    .attr(`height`, (s) => height - yScale(s.value))
    .attr(`width`, xScale.bandwidth())
複製程式碼

首先,我 selectAll 圖表上的所有元素,返回結果為空。然後,data 函式根據陣列長度通知 DOM 應該更新多少元素。如果資料個數多於 DOM 個數時,則 enter 會標識出缺少的元素。enter 會返回需要新增的元素。
通常,後面緊跟 append 方法會把元素新增到 DOM 中。

基本上,我用 D3.js 給陣列每一項都追加了一個矩形。

當前只在彼此頂部新增了沒有寬高的矩形。這兩個屬性必須通過之前的縮放函式計算所得。

我呼叫 attr 方法新增了矩形座標。第二個引數可以是回撥,它返回3個引數:當前繫結的資料,索引和所有資料陣列。

.attr(’x’, (actual, index, array) =>
    xScale(actual.value))
複製程式碼

縮放函式返回給定範圍值的座標。計算座標就是小菜一碟,訣竅是利用柱子的高度。必須從圖表的高度減去計算出的 y 座標,才能得到正確的列值。

定義矩形的寬度也會用到縮放函式。scaleBand 有一個 bandwidth 函式,它基於設定的間距返回一個元素的計算寬度。

D3.js 教程: 使用 JavaScript 建立可互動的柱狀圖

幹得不錯,但沒那麼花哨,對吧?

為了防止觀眾視覺疲勞,讓我們新增一些資訊改善下視覺效果!

製作柱狀圖的技巧

有一些基本規則值得一提。

  • 避免使用 3D 效果;
  • 直觀地排序資料點 – 按字母順序或按數字排序;
  • 柱條之間保持一定距離;
  • y 軸從 0 開始,而不是從最小值開始;
  • 使用統一的顏色;
  • 新增軸標籤、標題、導引線。

D3.js 網格系統

我想在背景中新增柵格線突出那些值。

垂直和水平的線都可以新增,我的建議是隻新增一種。過多的線會分散注意力。以下程式碼片段演示瞭如何新增水平和垂直的柵格。

chart.append(`g`)
    .attr(`class`, `grid`)
    .attr(`transform`, `translate(0, ${height})`)
    .call(d3.axisBottom()
        .scale(xScale)
        .tickSize(-height, 0, 0)
        .tickFormat(``))

chart.append(`g`)
    .attr(`class`, `grid`)
    .call(d3.axisLeft()
        .scale(yScale)
        .tickSize(-width, 0, 0)
        .tickFormat(``))
複製程式碼
D3.js 教程: 使用 JavaScript 建立可互動的柱狀圖

此例中,我更喜歡垂直柵格線,因為它可以引導視線,保持整體畫面簡介明快。

D3.js 中的標籤

我還想新增一些文字指導,從而使圖表更加全面。讓我們給圖表命個名,併為座標軸新增標籤吧。

D3.js 教程: 使用 JavaScript 建立可互動的柱狀圖

文字是 SVG 元素,同樣可以新增到 SVG 或者分組中。它們可以使用 x 和 y 座標定位,文字對齊是通過 text-anchor 屬性實現的。
新增標籤文字,只需呼叫文字元素上的 text 方法。

svg.append(`text`)
    .attr(`x`, -(height / 2) - margin)
    .attr(`y`, margin / 2.4)
    .attr(`transform`, `rotate(-90)`)
    .attr(`text-anchor`, `middle`)
    .text(`Love meter (%)`)

svg.append(`text`)
    .attr(`x`, width / 2 + margin)
    .attr(`y`, 40)
    .attr(`text-anchor`, `middle`)
    .text(`Most loved programming languages in 2018`)
複製程式碼

與 D3.js 互動

我們的圖表內容已然豐富,但是仍然可以新增些互動效果。

以下的程式碼演示瞭如何給 SVG 元素新增事件監聽。

svgElement
    .on(`mouseenter`, function (actual, i) {
        d3.select(this).attr(‘opacity’, 0.5)
    })
    .on(`mouseleave’, function (actual, i) {
        d3.select(this).attr(‘opacity’, 1)
    })
複製程式碼

注意,我用了函式表示式而不是箭頭函式,因為我通過 this 關鍵字訪問元素。

當滑鼠滑過選中的 SVG 元素時,它的透明度變為原始值的一半,滑鼠離開元素時透明度恢復原始值。

你也可以通過 d3.mouse 獲取滑鼠座標。它返回一個具有 x 和 y 座標的陣列。在游標所在位置顯示提示,就可以通過這個實現。

建立令人瞠目結舌的圖表並沒那麼簡單。

可能需要圖形設計師,UX 研究員和其他牛人的智慧。以下例子展示了幾個提升圖表效果的可能性!

我們的圖表顯示了非常相似的值,所以為了突出條形值之間的差異,我新增了一個 mouseenter 事件。每當使用者懸停在特定的列時,該欄的頂部就會畫一條水平線。此外,我還計算了與其他柱條的差異,並顯示在了相應的柱條上。

D3.js 教程: 使用 JavaScript 建立可互動的柱狀圖

很整齊吧?我還在此例中增加了透明度,加大了柱條的寬度。

.on(‘mouseenter’, function (s, i) {
    d3.select(this)
        .transition()
        .duration(300)
        .attr(`opacity`, 0.6)
        .attr(`x`, (a) => xScale(a.language) - 5)
        .attr(`width`, xScale.bandwidth() + 10)

    chart.append(`line`)
        .attr(`x1`, 0)
        .attr(`y1`, y)
        .attr(`x2`, width)
        .attr(`y2`, y)
        .attr(`stroke`, `red`)

    // 部分實現,整體效果見原始碼
})
複製程式碼

transition 方法表明我想把 DOM 改變繪製成動畫。它的時間間隔是用 duration 函式設定的,該函式以毫秒作為引數。上面的過渡會淡化帶狀顏色,並加寬條形的寬度。

要畫一條 SVG 線,我需要起點和終點。這可以通過 x1y1x2y2 座標來設定。直到我用 stroke 屬性設定線條的顏色,線條才可見。

這裡只展示了 mouseenter 事件這部分,切記,必須在 mouseout 事件上恢復或刪除更改。本文末尾提供了完整的原始碼。

讓我們給圖表新增一些樣式吧!

回顧下我們目前為止完成了那些功能,以及如何通過樣式裝扮圖表。可以通過先前用過的 attr 方法給 SVG 元素新增 class 屬性。

我們的圖表功能豐富,而不是死板的靜態圖片,滑鼠懸停時可以顯示各個柱條的差值。標題交代表格的背景,標籤幫助識別座標軸的測量單位。我還在右下角新增了新的標籤,註明資料來源。

剩下的事情就差顏色和字型了!

深色背景的圖表使亮色柱條看起來很酷。我還使用了 open Sans 字型,並給不同的標籤設定不同的大小和粗細。

注意到那條虛線了嗎?它是通過 stroke-widthstroke-dasharray 屬性實現的。使用 stroke-dasharray,可以定義虛線的圖案和間距,從而改變形狀的輪廓。

line#limit {
    stroke: #FED966;
    stroke-width: 3;
    stroke-dasharray: 3 6;
}

.grid path {
    stroke-width: 3;
}

.grid .tick line {
    stroke: #9FAAAE;
    stroke-opacity: 0.2;
}
複製程式碼

網格線比較討巧,我給分組中的路徑元素使用了 stroke-width: 0,為了隱藏表格的框架,我還通過設定線條的透明度降低它們的可見性。

所有其它有關字型大小和顏色的 CSS 可以參照原始碼。

收尾我們的 D3.js 柱狀圖教程

D3.js 是一個令人驚歎的 DOM 操作庫。它的內部埋藏了無數的寶藏等待你去探索(確切的說,不是埋藏,文件也很齊全)。此文僅僅使用了它的工具集的冰山一角,就建立了一個不同凡響的柱狀圖。

繼續探索吧,定能創造出無比壯觀的視覺效果!

這是本文示例原始碼 的連結。

你用 D3.js 做過一些炫酷的東西嗎?和我們分享一下!你有任何問題,或者想要關於這個主題的另一個教程,歡迎留言!

謝謝閱讀,下次再見!

D3.js 教程: 使用 JavaScript 建立可互動的柱狀圖

相關文章