手把手帶你上手D3.js數據可視化系列(二)

前言

上一篇文章「手把手帶你上手D3.js數據可視化系列(一) - 牛衣古柳 2021.07.30」裏古柳介紹瞭如何添加並設置 SVG 畫布、添加矩形元素、根據數據集來添加多個矩形元素、運用取餘取整操做調整佈局並換行顯示等內容。javascript

文章最後留下一個疑問,就是可否基於數據集大小和畫布大小來自動計算出每一個rect的寬高和間距,而後自動佈局?html

正好古柳以前啃大西洋手抄本可視化做品源碼時看到了相關實現方法,這裏就和你們分享下。 相關閱讀:迄今復現過最複雜的可視化做品之「大西洋古抄本」(上) - 牛衣古柳 2021.06.17迄今復現過最複雜的可視化做品之「大西洋古抄本」(下) - 牛衣古柳 2021.06.22vue

不過古柳也沒有吃透背後的原理,只能儘可能寫下本身的理解,並且一來你們不必定會用到這個自動佈局的方法,二來真要用到直接 copy 拿走也不是不能夠,因此若是這部分最終也沒搞懂其實問題不大,對後續沒啥影響,放心。下一篇會回到基礎的 D3.js 數據可視化的講解上。java

基礎代碼

首先基本代碼結構和上一篇文章相似,有不懂的地方能夠回顧下:「手把手帶你上手D3.js數據可視化系列(一) - 牛衣古柳 2021.07.30」git

此次 SVG 畫布撐滿網頁窗口大小,寬度再也不是一半大小;而且 dataset 數據集設置大些,即 [0, 1, 2, ..., 99] 共100條數據,不事後面會自動基於數據量大小計算佈局,因此數據多少並不重要;另外 colors 顏色數組不變,繪製矩形時仍會經過取餘數的方式來取對應顏色,之後也會介紹顏色比例尺,將類別屬性進行映射到對應顏色,到時候再說。github

<body>
    <div id="chart"></div>
    <script src="./d3.js"></script>
    <script> function drawChart() { const width = window.innerWidth const height = window.innerHeight const svg = d3.select('#chart') .append('svg') .attr('width', width) .attr('height', height) .style('background', '#FEF5E5') const dataset = d3.range(100) console.log(dataset) // [0, 1, 2, ..., 99] const colors = ['#00AEA6', '#DB0047', '#F28F00', '#EB5C36', '#242959', '#2965A7'] // .... } drawChart() </script>
</body>
複製代碼

自動佈局之計算矩形寬度

畫布設置好後,先來總體看看大西洋手抄本可視化做品源碼裏是如何根據畫布大小和數據多少計算每一個矩形的寬度 rectWidth 的,因爲矩形高度均是寬度的1.5倍,因此無需另外計算。(注意:這部分代碼並不是徹底和源碼裏一致,不少變量名等都爲了講解方便從新改了下,但邏輯一致、計算流程相同)數組

const containerWidth = width
const containerHeight = height
const containerArea = containerWidth * containerHeight

const halfMargin = (containerWidth / 100) * 0.3
const totalMargin = halfMargin * 2

let rectWidth = Math.sqrt(containerArea / (1.5 * dataset.length)) - totalMargin

const columns = containerWidth / (rectWidth + totalMargin)
const rows = dataset.length / columns
const rest = dataset.length % parseInt(columns)

if (rest <= rows) {
    rectWidth = containerWidth / (columns + 1) - totalMargin
} else if (rest > rows) {
    rectWidth = containerWidth / (columns + 2) - totalMargin
}
複製代碼

接下來拆解代碼,看看都作了哪些事。微信

畫布容器面積

首先,計算出畫布容器的面積 containerArea。這裏 containerWidthcontainerHeight 分別對應 widthheight,彷佛畫蛇添足。但有時候畫布寬高並非手動設置的,而是經過 getBoundingClientRect() 獲取元素的寬高後進行指定,相似這樣的方式 containerWidth = svg.getBoundingClientRect().widthcontainerHeight = svg.getBoundingClientRect().height。總之知道這裏要先計算出面積便可。
連接:https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRectmarkdown

const containerWidth = width
const containerHeight = height
const containerArea = containerWidth * containerHeight
複製代碼

空白間距

接着計算出矩形之間的空白間距。這裏矩形上下左右一圈的 halfMargin 是經過容器寬度 containerWidth 計算出來的,即 (containerWidth / 100) * 0.3,可見容器寬度越大間距越大,反之亦然;totalMargin 就是左邊+右邊或者上邊+下邊的間距,也就是 halfMargin 的2倍。app

const halfMargin = (containerWidth / 100) * 0.3
const totalMargin = halfMargin * 2
複製代碼

此時每一個矩形包含間距後的總體寬度是 rectWidth + totalMargin,總體高度是 1.5 * rectWidth + totalMargin(上面說過矩形實際高度老是寬度的1.5倍)。

初步算出矩形實際寬度

而後源碼裏經過下面的公式初步算出矩形實際寬度 rectWidth,能夠看出來大概是想經過全部矩形總體面積等於容器面積的方式,但彷佛又有點不一樣。

// 初步計算出矩形實際寬度
let rectWidth = Math.sqrt(containerArea / (1.5 * dataset.length)) - totalMargin

// 變換後
// (rectWidth + totalMargin) * 1.5 * (rectWidth + totalMargin) * dataset.length = containerArea
複製代碼

論理,單個矩形總體面積 = 總體寬度 * 總體寬度 = (rectWidth + totalMargin) * (1.5 * rectWidth + totalMargin),原始面積公式應該以下,而源碼裏彷佛採用了近似後的計算公式,古柳猜想多是基於簡化計算的緣由,不然照原始公式還要解一元二次方程才能算出 rectWidth。並且後面實際繪製矩形時,就會發現確實是矩形實際高度爲實際寬度的1.5倍,而不是總體高度爲總體寬度的1.5倍,因此可知這裏是近似後,應該就是爲了簡化計算。

// 原始面積計算公式
(rectWidth + totalMargin) * (1.5 * rectWidth + totalMargin) * dataset.length = containerArea

// 近似後直接算出,不用解一元二次方程
(rectWidth + totalMargin) * 1.5 * (rectWidth + totalMargin) * dataset.length = containerArea
複製代碼

矩形最終寬度

上面說初步計算出矩形實際寬度 rectWidth,是由於這裏還經過下面的方式,在比較 rowsrest 孰大孰小後,算出最終 rectWidth。首先是根據容器寬度除以單個矩形總體寬度獲得 columns,因爲這裏沒有向下取整,因此帶有小數;接着根據數據多少,算出 rows,一樣帶有小數;而後根據數據多少和向下取整後的 columns 算出 rest;最後若是 rest <= rest 則列數多加一列,不然多加兩列,而後計算出最終矩形寬度 rectWidth

let rectWidth = Math.sqrt(containerArea / (1.5 * dataset.length)) - totalMargin

const columns = containerWidth / (rectWidth + totalMargin)
const rows = dataset.length / columns
const rest = dataset.length % parseInt(columns)

if (rest <= rows) {
    rectWidth = containerWidth / (columns + 1) - totalMargin
} else if (rest > rows) {
    rectWidth = containerWidth / (columns + 2) - totalMargin
}
複製代碼

其實這步古柳就不懂爲什麼這樣算了,雖然能夠馬後炮地說,這樣確實能避免矩形超出畫布,並且能儘可能佔滿畫布空間,但不肯定背後原理。(若是有人看懂了的話能夠羣裏告訴古柳!)

但古柳想到相似上篇文章「手把手帶你上手D3.js數據可視化系列(一) - 牛衣古柳 2021.07.30」裏調整佈局,換行顯示的部分,若是這裏也分別對寬高進行限制,即每一行的最後一個矩形總體要在畫布內,而且每一列的最後一個矩形總體要在畫布內,而後列下公式,看看能不能計算出來。不過這裏暫時不嘗試了,先以介紹大西洋手抄本裏的源碼爲主。

繪製矩形

算出矩形實際寬度 rectWidth 後,高度也就知道了;這裏從新設置空白間距 rectTotalMargin,而後獲得帶間距矩形總體的寬高 rectTotalWidthrectTotalHeight;接着容器寬度除以單個矩形總體寬度,並向下取整,就是每行最後矩形個數 columnNum;最後繪製矩形一樣用這三個步驟 svg.selectAll('rect').data(dataset).join('rect'),而且採用取餘取整操做,計算出每一個矩形的x/y座標值,和上一票最後調整佈局換行顯示的都相似,應該無需過多解釋了。

const rectHeight = 1.5 * rectWidth
const rectTotalMargin = containerWidth * 0.005
const rectTotalWidth = rectWidth + rectTotalMargin
const rectTotalHeight = rectHeight + rectTotalMargin

const columnNum = Math.floor(containerWidth / rectTotalWidth)

const rects = svg.selectAll('rect')
    .data(dataset)
    .join('rect')
    .attr('x', d => rectTotalMargin + d % columnNum * rectTotalWidth)
    .attr('y', d => rectTotalMargin + Math.floor(d / columnNum) * rectTotalHeight)
    .attr('width', rectWidth)
    .attr('height', rectHeight)
    .attr('fill', d => colors[d % colors.length])
複製代碼

源碼裏是組件化方式實現

這裏可能須要提下,大西洋古抄本源碼是用 Vue 框架實現的,可視化部分用的 Vue-Konva。源碼裏是在父組件裏算出矩形實際寬度 rectWidth,也就是下面的 elementWidth後,將數據傳遞給子組件 PageVizCanvas 而後由該組件完成可視化功能,因此像上面的空白間距又從新設置了一遍等操做,也是子組件裏進行的,雖然不肯定爲何這裏乘以0.005,和前面的又不一致了,但沒出啥bug就先隨它去吧。 連接:https://cn.vuejs.org/ 連接:https://github.com/konvajs/vue-konva

<PageVizCanvas
    :inputData="filteredData"
    :viewPages="viewPages"
    :width="elementWidth"
    :height="1.5 * elementWidth"
    :activePages="activePages"
    :navigateTo="navigateTo"
/>
複製代碼

固然新手對 Vue 框架和組件化開發等不瞭解,能夠暫時忽略。

小結

文章也不短了,做爲本系列的第二篇文章,古柳簡單分享了下優秀可視化做品源碼裏涉及的基於數據集大小和畫布大小來自動佈局的方法。誠然在古柳本身也沒徹底理解的狀況下,就這麼寫出來彷佛並很差,但仍是那句話,本系列都是按照古柳本身想寫的邏輯來寫的,接着上篇文章的順序,就以爲一切並不突兀、比較瓜熟蒂落,那就寫寫吧,等下一篇會回到基礎的 D3.js 數據可視化的講解上。

另外,若是有人能搞懂上述源碼裏的方法、或者有什麼其餘方法,也歡迎告訴古柳、羣裏交流。

照例

若是歡迎來「可視化交流羣」一塊兒交流,加古柳微信「xiaoaizhj」備註「可視化加羣」拉你進羣哈!

歡迎關注古柳的公衆號「牛衣古柳」,並設置星標,以便第一時間收到更新。

相關閱讀:安利一些不錯的D3.js數據可視化資源

相關文章
相關標籤/搜索