哥爬蟲
大數(shù)據(jù)時(shí)代,各行各業(yè)對(duì)數(shù)據(jù)采集的需求日益增多,網(wǎng)絡(luò)爬蟲的運(yùn)用也更為廣泛,越來越多的人開始學(xué)習(xí)網(wǎng)絡(luò)爬蟲這項(xiàng)技術(shù),K哥爬蟲此前已經(jīng)推出不少爬蟲進(jìn)階、逆向相關(guān)文章,為實(shí)現(xiàn)從易到難全方位覆蓋,特設(shè)【0基礎(chǔ)學(xué)爬蟲】專欄,幫助小白快速入門爬蟲,本期為網(wǎng)頁基本結(jié)構(gòu)介紹。
網(wǎng)頁是互聯(lián)網(wǎng)應(yīng)用的一種形態(tài),是組成網(wǎng)站的基本元素。它是一個(gè)包含HTML標(biāo)簽的純文本文件,可以存放在世界上任意一臺(tái)計(jì)算機(jī)中。網(wǎng)頁可以被看作為承載各種網(wǎng)站應(yīng)用和信息的容器,網(wǎng)站的可視化信息都通過網(wǎng)頁來進(jìn)行展示,為網(wǎng)站用戶提供一個(gè)友好的界面。
表面上,網(wǎng)頁的組成可以分為文字、圖片、音頻、視頻、超鏈接等元素構(gòu)成,這些元素是用戶能夠直接看到的。但在本質(zhì)上,網(wǎng)頁的組成分為三部分:
HTML的全稱為超文本標(biāo)記語言,是一種標(biāo)記語言,它是標(biāo)準(zhǔn)通用標(biāo)記語言下的一個(gè)應(yīng)用,也是一種規(guī)范,一種標(biāo)準(zhǔn),它通過標(biāo)記符號(hào)來標(biāo)記要顯示的網(wǎng)頁中的各個(gè)部分。HTML文本是由HTML命令組成的描述性文本,HTML命令可以說明文字、圖片、音頻、視頻、超鏈接等,用戶在網(wǎng)頁上看到的各種元素都是通過HTML文本來實(shí)現(xiàn)的。
網(wǎng)頁的基本元素是通過HTML來實(shí)現(xiàn)的,但是HTML只能實(shí)現(xiàn)最基本的網(wǎng)頁樣式。隨著HTML的發(fā)展,為了滿足網(wǎng)頁開發(fā)者的需求,CSS便孕育而生。
CSS全稱為層疊樣式表。它為HTML語言提供了一種樣式描述,定義了元素的顯示方式。提供了豐富的樣式定義以及設(shè)置文本和背景屬性的能力。CSS可以將所有的樣式聲明統(tǒng)一存放,進(jìn)行統(tǒng)一管理。在CSS中,一個(gè)文件的樣式可以從其他的樣式表中繼承。讀者在有些地方可以使用他自己更喜歡的樣式,在其他地方則繼承或“層疊”作者的樣式。這種層疊的方式使作者和讀者都可以靈活地加入自己的設(shè)計(jì),混合每個(gè)人的愛好。
JavaScript(JS)是一種面向?qū)ο蟮慕忉屝湍_本語言,它具有簡單、動(dòng)態(tài)、跨平臺(tái)的特點(diǎn)。它被廣泛應(yīng)用與Web開發(fā)中,幫助開發(fā)者構(gòu)建可拓展的交互式Web應(yīng)用。JavaScript由三部分組成:
網(wǎng)頁的基本結(jié)構(gòu)大致可以分為四部分:Doctype聲明、html元素、head元素和body元素。
元素(Element)是網(wǎng)頁的一部分,是構(gòu)成網(wǎng)頁的基本單位,實(shí)際上一個(gè)網(wǎng)頁就是由多個(gè)元素構(gòu)成的的文本文件。 標(biāo)簽(Tag)的作用就是用來定義元素。大多數(shù)的標(biāo)簽都是成對(duì)使用的,它存在一個(gè)開始標(biāo)簽與一個(gè)結(jié)尾標(biāo)簽,開始與結(jié)尾標(biāo)簽中間包含該元素的文本信息。
<div>這是一個(gè)div標(biāo)簽</div>
<p>這是一個(gè)p標(biāo)簽</p>
也有少部分的標(biāo)簽不成對(duì)。
<input>
<img>
<hr>
...
屬性(attribute)主要是用來為標(biāo)簽添加額外的信息,屬性的定義一般在開始標(biāo)簽中,以鍵值對(duì)的形式出現(xiàn)(name="value" ),屬性的值應(yīng)始終包括在引號(hào)內(nèi),屬性和屬性值對(duì)大小寫不敏感,但是推薦使用小寫的屬性與屬性值。一個(gè)標(biāo)簽可以擁有多個(gè)屬性,也可以沒有屬性,開發(fā)者沒有為標(biāo)簽定義屬性的話則會(huì)使用默認(rèn)屬性。
<a href="https://www.kuaidaili.com/">這是一個(gè)a標(biāo)簽,href是我的屬性。</a>
屬性在HTML中被分為兩種:通用屬性和專用屬性。 通用屬性適用于大部分或所有標(biāo)簽之中,如:
專用屬性適用于小部分標(biāo)簽或特定標(biāo)簽,如:
DOM全稱即文檔對(duì)象模型,是W3C制定的標(biāo)準(zhǔn)接口規(guī)范,是一種處理HTML和XML文件的標(biāo)準(zhǔn)API。DOM將HTML文本作為一個(gè)樹形結(jié)構(gòu),DOM樹的每個(gè)結(jié)點(diǎn)都表示了一個(gè)HTML標(biāo)簽或HTML標(biāo)簽內(nèi)的文本項(xiàng),它將網(wǎng)頁與腳本或編程語言連接起來。
通過這個(gè)DOM樹,開發(fā)者可以通過JavaScript來創(chuàng)建動(dòng)態(tài)HTML,開發(fā)者借助JavaScript可以實(shí)現(xiàn):
DOM提供了一系列API來實(shí)現(xiàn)這些操作。
css選擇器是用來對(duì)HTML頁面中的元素進(jìn)行控制,通過對(duì)CSS選擇器的了解,可以加深對(duì)網(wǎng)頁結(jié)構(gòu)與節(jié)點(diǎn)的理解。常用的CSS選擇器主要分為:
1、元素選擇器: 通過標(biāo)簽名{}的格式來選中對(duì)應(yīng)標(biāo)簽,如:p{}。
2、類選擇器: 通過.類名{}的格式來選中對(duì)應(yīng)類名的標(biāo)簽,如:.page{},page為元素的類名。
3、id選擇器: 通過#id值{}的格式來選中對(duì)應(yīng)id值的標(biāo)簽,如:#key{},key為元素的id值。
4、群組選擇器: 通過選擇器1,選擇器2,選擇器3...{}的格式來選中對(duì)應(yīng)選擇器的標(biāo)簽,如:div,.page{},即選擇div標(biāo)簽下類名為pagae的標(biāo)簽。
5、子元素選擇器: 通過父元素 > 子元素{}的格式來選中對(duì)應(yīng)父元素中對(duì)應(yīng)子元素的標(biāo)簽,如:div > p{},即選擇div標(biāo)簽下的p標(biāo)簽,子元素選擇器只能選擇直接后代,不能跨節(jié)點(diǎn)選取。
6、后代選擇器: 通過父元素 子元素{}的格式來選中對(duì)應(yīng)父元素中對(duì)應(yīng)子元素的標(biāo)簽,如:div p{},即選擇div標(biāo)簽下的p標(biāo)簽,后代選擇器可以跨節(jié)點(diǎn)選取。
談一個(gè)網(wǎng)頁打開的全過程(涉及DNS、CDN、Nginx負(fù)載均衡等)
從用戶在瀏覽器輸入域名開始,到web頁面加載完畢,這是一個(gè)說復(fù)雜不復(fù)雜,說簡單不簡單的過程,下文暫且把這個(gè)過程稱作網(wǎng)頁加載過程。下面我將依靠自己的經(jīng)驗(yàn),總結(jié)一下整個(gè)過程。如有錯(cuò)漏,歡迎指正。
閱讀本文需要讀者已有一定的計(jì)算機(jī)知識(shí),了解TCP、DNS等。
眾所周知,打開一個(gè)網(wǎng)頁的過程中,瀏覽器會(huì)因頁面上的css/js/image等靜態(tài)資源會(huì)多次發(fā)起連接請(qǐng)求,所以我們暫且把這個(gè)網(wǎng)頁加載過程分成兩部分:
2.1 頁面加載
先上一張圖,直觀明了地讓大家了解下基本流程,然后我們?cè)僦鹨环治觥?/p>
2.1.1 DNS解析
什么是DNS解析?當(dāng)用戶輸入一個(gè)網(wǎng)址并按下回車鍵的時(shí)候,瀏覽器得到了一個(gè)域名。而在實(shí)際通信過程中,我們需要的是一個(gè)IP地址。因此我們需要先把域名轉(zhuǎn)換成相應(yīng)的IP地址,這個(gè)過程稱作DNS解析。
1) 瀏覽器首先搜索瀏覽器自身緩存的DNS記錄。
或許很多人不知道,瀏覽器自身也帶有一層DNS緩存。Chrome 緩存1000條DNS解析結(jié)果,緩存時(shí)間大概在一分鐘左右。
(Chrome瀏覽器通過輸入:chrome://net-internals/#dns 打開DNS緩存頁面)
2) 如果瀏覽器緩存中沒有找到需要的記錄或記錄已經(jīng)過期,則搜索hosts文件和操作系統(tǒng)緩存。
在Windows操作系統(tǒng)中,可以通過 ipconfig /displaydns 命令查看本機(jī)當(dāng)前的緩存。
通過hosts文件,你可以手動(dòng)指定一個(gè)域名和其對(duì)應(yīng)的IP解析結(jié)果,并且該結(jié)果一旦被使用,同樣會(huì)被緩存到操作系統(tǒng)緩存中。
Windows系統(tǒng)的hosts文件在%systemroot%\system32\drivers\etc下,linux系統(tǒng)的hosts文件在/etc/hosts下。
3) 如果在hosts文件和操作系統(tǒng)緩存中沒有找到需要的記錄或記錄已經(jīng)過期,則向域名解析服務(wù)器發(fā)送解析請(qǐng)求。
其實(shí)第一臺(tái)被訪問的域名解析服務(wù)器就是我們平時(shí)在設(shè)置中填寫的DNS服務(wù)器一項(xiàng),當(dāng)操作系統(tǒng)緩存中也沒有命中的時(shí)候,系統(tǒng)會(huì)向DNS服務(wù)器正式發(fā)出解析請(qǐng)求。這里是真正意義上開始解析一個(gè)未知的域名。
一般一臺(tái)域名解析服務(wù)器會(huì)被地理位置臨近的大量用戶使用(特別是ISP的DNS),一般常見的網(wǎng)站域名解析都能在這里命中。
4) 如果域名解析服務(wù)器也沒有該域名的記錄,則開始遞歸+迭代解析。
這里我們舉個(gè)例子,如果我們要解析的是mail.google.com。
首先我們的域名解析服務(wù)器會(huì)向根域服務(wù)器(全球只有13臺(tái))發(fā)出請(qǐng)求。顯然,僅憑13臺(tái)服務(wù)器不可能把全球所有IP都記錄下來。所以根域服務(wù)器記錄的是com域服務(wù)器的IP、cn域服務(wù)器的IP、org域服務(wù)器的IP……。如果我們要查找.com結(jié)尾的域名,那么我們可以到com域服務(wù)器去進(jìn)一步解析。所以其實(shí)這部分的域名解析過程是一個(gè)樹形的搜索過程。
根域服務(wù)器告訴我們com域服務(wù)器的IP。
接著我們的域名解析服務(wù)器會(huì)向com域服務(wù)器發(fā)出請(qǐng)求。根域服務(wù)器并沒有mail.google.com的IP,但是卻有google.com域服務(wù)器的IP。
接著我們的域名解析服務(wù)器會(huì)向google.com域服務(wù)器發(fā)出請(qǐng)求。...
如此重復(fù),直到獲得mail.google.com的IP地址。
為什么是遞歸:問題由一開始的本機(jī)要解析mail.google.com變成域名解析服務(wù)器要解析mail.google.com,這是遞歸。
為什么是迭代:問題由向根域服務(wù)器發(fā)出請(qǐng)求變成向com域服務(wù)器發(fā)出請(qǐng)求再變成向google.com域發(fā)出請(qǐng)求,這是迭代。
5) 獲取域名對(duì)應(yīng)的IP后,一步步向上返回,直到返回給瀏覽器。
2.1.2 發(fā)起TCP請(qǐng)求
瀏覽器會(huì)選擇一個(gè)大于1024的本機(jī)端口向目標(biāo)IP地址的80端口發(fā)起TCP連接請(qǐng)求。經(jīng)過標(biāo)準(zhǔn)的TCP握手流程,建立TCP連接。
關(guān)于TCP協(xié)議的細(xì)節(jié),這里就不再闡述。這里只是簡單地用一張圖說明一下TCP的握手過程。如果不了解TCP,可以選擇跳過此段,不影響本文其他部分的瀏覽。
2.1.3 發(fā)起HTTP請(qǐng)求
其本質(zhì)是在建立起的TCP連接中,按照HTTP協(xié)議標(biāo)準(zhǔn)發(fā)送一個(gè)索要網(wǎng)頁的請(qǐng)求。
2.1.4 負(fù)載均衡
什么是負(fù)載均衡?當(dāng)一臺(tái)服務(wù)器無法支持大量的用戶訪問時(shí),將用戶分?jǐn)偟絻蓚€(gè)或多個(gè)服務(wù)器上的方法叫負(fù)載均衡。
什么是Nginx?Nginx是一款面向性能設(shè)計(jì)的HTTP服務(wù)器,相較于Apache、lighttpd具有占有內(nèi)存少,穩(wěn)定性高等優(yōu)勢(shì)。
負(fù)載均衡的方法很多,Nginx負(fù)載均衡、LVS-NAT、LVS-DR等。這里,我們以簡單的Nginx負(fù)載均衡為例。關(guān)于負(fù)載均衡的多種方法詳情大家可以Google一下。
Nginx有4種類型的模塊:core、handlers、filters、load-balancers。
我們這里討論其中的2種,分別是負(fù)責(zé)負(fù)載均衡的模塊load-balancers和負(fù)責(zé)執(zhí)行一系列過濾操作的filters模塊。
1) 一般,如果我們的平臺(tái)配備了負(fù)載均衡的話,前一步DNS解析獲得的IP地址應(yīng)該是我們Nginx負(fù)載均衡服務(wù)器的IP地址。所以,我們的瀏覽器將我們的網(wǎng)頁請(qǐng)求發(fā)送到了Nginx負(fù)載均衡服務(wù)器上。
2) Nginx根據(jù)我們?cè)O(shè)定的分配算法和規(guī)則,選擇一臺(tái)后端的真實(shí)Web服務(wù)器,與之建立TCP連接、并轉(zhuǎn)發(fā)我們?yōu)g覽器發(fā)出去的網(wǎng)頁請(qǐng)求。
Nginx默認(rèn)支持 RR輪轉(zhuǎn)法 和 ip_hash法 這2種分配算法。
前者會(huì)從頭到尾一個(gè)個(gè)輪詢所有Web服務(wù)器,而后者則對(duì)源IP使用hash函數(shù)確定應(yīng)該轉(zhuǎn)發(fā)到哪個(gè)Web服務(wù)器上,也能保證同一個(gè)IP的請(qǐng)求能發(fā)送到同一個(gè)Web服務(wù)器上實(shí)現(xiàn)會(huì)話粘連。
也有其他擴(kuò)展分配算法,如:
fair:這種算法會(huì)選擇相應(yīng)時(shí)間最短的Web服務(wù)器
url_hash:這種算法會(huì)使得相同的url發(fā)送到同一個(gè)Web服務(wù)器
3) Web服務(wù)器收到請(qǐng)求,產(chǎn)生響應(yīng),并將網(wǎng)頁發(fā)送給Nginx負(fù)載均衡服務(wù)器。
4) Nginx負(fù)載均衡服務(wù)器將網(wǎng)頁傳遞給filters鏈處理,之后發(fā)回給我們的瀏覽器。
而Filter的功能可以理解成先把前一步生成的結(jié)果處理一遍,再返回給瀏覽器。比如可以將前面沒有壓縮的網(wǎng)頁用gzip壓縮后再返回給瀏覽器。
2.1.5 瀏覽器渲染
1) 瀏覽器根據(jù)頁面內(nèi)容,生成DOM Tree。根據(jù)CSS內(nèi)容,生成CSS Rule Tree(規(guī)則樹)。調(diào)用JS執(zhí)行引擎執(zhí)行JS代碼。
2) 根據(jù)DOM Tree和CSS Rule Tree生成Render Tree(呈現(xiàn)樹)
3) 根據(jù)Render Tree渲染網(wǎng)頁
但是在瀏覽器解析頁面內(nèi)容的時(shí)候,會(huì)發(fā)現(xiàn)頁面引用了其他未加載的image、css文件、js文件等靜態(tài)內(nèi)容,因此開始了第二部分。
2.2 網(wǎng)頁靜態(tài)資源加載
以阿里巴巴的淘寶網(wǎng)首頁的logo為例,其url地址為 img.alicdn.com/tps/i2/TB1bNE7LFXXXXaOXFXXwFSA1XXX-292-116.png_145x145.jpg
我們清楚地看到了url中有cdn字樣。
什么是CDN?如果我在廣州訪問杭州的淘寶網(wǎng),跨省的通信必然造成延遲。如果淘寶網(wǎng)能在廣東建立一個(gè)服務(wù)器,靜態(tài)資源我可以直接從就近的廣東服務(wù)器獲取,必然能提高整個(gè)網(wǎng)站的打開速度,這就是CDN。CDN叫內(nèi)容分發(fā)網(wǎng)絡(luò),是依靠部署在各地的邊緣服務(wù)器,使用戶就近獲取所需內(nèi)容,降低網(wǎng)絡(luò)擁塞,提高用戶訪問響應(yīng)速度。
接下來的流程就是瀏覽器根據(jù)url加載該url下的圖片內(nèi)容。本質(zhì)上是瀏覽器重新開始第一部分的流程,所以這里不再重復(fù)闡述。區(qū)別只是負(fù)責(zé)均衡服務(wù)器后端的服務(wù)器不再是應(yīng)用服務(wù)器,而是提供靜態(tài)資源的服務(wù)器。
文章乃參考、轉(zhuǎn)載其他博客所得,僅供自己學(xué)習(xí)作筆記使用!!!
在本書的前幾章中,我們使用數(shù)據(jù)可視化來編碼數(shù)值數(shù)據(jù)。例如,第3章條形圖中條形的長度表示相關(guān)調(diào)查回復(fù)的數(shù)量。同樣,第4章折線圖中數(shù)據(jù)點(diǎn)的位置描繪了溫度。在本章中,我們將討論分層可視化,它對(duì)父子關(guān)系進(jìn)行編碼,并且可以揭示迄今為止我們使用的更簡單的可視化所沒有注意到的模式。
分層可視化通過外殼、連接或鄰接來傳達(dá)父子關(guān)系。圖 11.1 顯示了使用外殼的分層可視化的兩個(gè)示例:圓形包和樹狀圖。顧名思義,圓包是一組圓圈。有一個(gè)根父級(jí),最外圈,以及稱為節(jié)點(diǎn)的后續(xù)子級(jí)。節(jié)點(diǎn)的所有子節(jié)點(diǎn)都“打包”到該節(jié)點(diǎn)中,圓圈的大小與它們包含的節(jié)點(diǎn)數(shù)成正比。葉節(jié)點(diǎn)的大小(最低級(jí)別的子節(jié)點(diǎn))可以表示任意屬性。樹狀圖的工作方式類似,但使用嵌套矩形而不是圓形。樹狀圖比圓形包更節(jié)省空間,我們經(jīng)常在與財(cái)務(wù)相關(guān)的可視化中遇到它們。
可視化父子關(guān)系的一種熟悉且直觀的方法是通過連接,例如在樹形圖中。樹形圖可以是線性的,如家譜樹,也可以是徑向的,如圖 11.1 所示。線性樹形圖更易于閱讀,但會(huì)占用大量空間,而徑向樹更緊湊,但需要更多的努力來破譯。
最后,我們可以通過冰柱圖(也稱為分區(qū)層圖)的鄰接來可視化層次結(jié)構(gòu)模式。我們經(jīng)常在IT(信息技術(shù))中遇到這樣的圖表。
圖 11.1 中顯示的圖表可能看起來多種多樣,但使用 D3 構(gòu)建它們意味著涉及布局生成器函數(shù)的類似過程。在第 5 章中,我們了解了 D3 的布局生成器函數(shù)如何將信息添加到現(xiàn)有數(shù)據(jù)集,以及我們可以使用此信息將所需的形狀附加到 SVG 容器中。創(chuàng)建分層可視化也不例外。
在本章中,我們將構(gòu)建兩個(gè)分層可視化:圓形包和線性樹形圖。我們將基于世界上 100 種使用最多的語言的數(shù)據(jù)集進(jìn)行可視化。您可以看到我們將在 https://d3js-in-action-third-edition.github.io/100-most-spoken-languages/ 構(gòu)建的圖表。
我們數(shù)據(jù)集中的每種語言都屬于一個(gè)語言家族或一組從共同祖先發(fā)展而來的相關(guān)語言。這些家族可以細(xì)分為稱為分支的較小組。讓我們以五種最常用的語言為例。在表 11.1 中,我們看到了如何將每種語言的信息存儲(chǔ)在電子表格中。左列包含語言:英語、中文普通話、印地語、西班牙語和法語。以下列包括相關(guān)的語系:印歐語系和漢藏語系,以及語言分支:日耳曼語系、漢尼特語系、印度-雅利安語系和羅曼語系。我們用每種語言的使用者總數(shù)和母語人士的數(shù)量來完成表格。
語言 | 家庭 | 分支 | 演講者總數(shù) | 母語人士 |
英語 | 印歐語系 | 日耳曼 | 1,132m | 379m |
普通話 | 漢藏語 | 西尼特 | 1,117m | 918m |
印地語 | 印歐語系 | 印度-雅利安語 | 615m | 341m |
西班牙語 | 印歐語系 | 浪漫 | 534m | 460m |
法語 | 印歐語系 | 浪漫 | 280m | 77m |
分層可視化具有單個(gè)根節(jié)點(diǎn),該節(jié)點(diǎn)分為多個(gè)以葉結(jié)尾的分支。在表 11.1 的示例數(shù)據(jù)集中,根節(jié)點(diǎn)可以稱為“語言”,如圖 11.2 所示。詞根分為兩個(gè)語系:印歐語系和漢藏語系,也分為分支:日耳曼語系、印度-雅利安語系、羅曼語系和漢尼語系。最后,葉子出現(xiàn)在圖形的右側(cè):英語、印地語、西班牙語、法語和普通話。每種語言、分支、族和根稱為一個(gè)節(jié)點(diǎn)。
在本書的前半部分,我們主要使用類似遺產(chǎn)的項(xiàng)目結(jié)構(gòu)。主要目標(biāo)是進(jìn)行簡單的設(shè)置,并專注于D3。但是,如果您發(fā)布 D3 項(xiàng)目,則很有可能使用 JavaScript 模塊導(dǎo)入。在本章中,我們將對(duì)項(xiàng)目結(jié)構(gòu)進(jìn)行現(xiàn)代化改造,以允許單獨(dú)導(dǎo)入 D3 模塊。它將使我們的項(xiàng)目文件更小,因此加載速度更快,并且將是查看哪些 D3 模塊包含哪種方法的絕佳機(jī)會(huì)。這些知識(shí)將使您將來更容易搜索 D3 文檔。
要將我們的 JavaScript 文件和 npm 模塊組合成一個(gè)瀏覽器可讀的模塊,我們需要一個(gè)捆綁器。您可能已經(jīng)熟悉 Webpack 或 RollUp。由于此類工具可能需要相當(dāng)多的配置,因此我們將轉(zhuǎn)向Parcel(https://parceljs.org/),這是一個(gè)非常易于使用且需要接近零的配置的捆綁器。
如果您的計(jì)算機(jī)上尚未安裝 Parcel,則可以使用以下命令對(duì)其進(jìn)行全局安裝,其中 -g 代表全局。在終端窗口中運(yùn)行此命令。
npm install -g parcel
我們建議使用此類全局安裝,因?yàn)樗鼘⑹?Parcel 可用于您的所有項(xiàng)目。請(qǐng)注意,根據(jù)計(jì)算機(jī)的配置,您可能需要在命令的開頭添加 Mac 和 Linux 上的術(shù)語 sudo 或 Windows 上的 runas。
在您的計(jì)算機(jī)上安裝 Parcel 后,在代碼編輯器 (https://github.com/d3js-in-action-third-edition/code-files/tree/main/chapter_11/11.1-Formatting_hierarchical_data/start) 中打開本章代碼文件的起始文件夾。如果您使用的是 VS Code,請(qǐng)打開集成終端并運(yùn)行命令 npm install 以安裝項(xiàng)目依賴項(xiàng)。在此階段,我們唯一的依賴項(xiàng)是允許我們稍后加載CSV數(shù)據(jù)文件。
若要啟動(dòng)項(xiàng)目,請(qǐng)運(yùn)行命令包,后跟根文件的路徑:
parcel src/index.html
在瀏覽器中打開 http://localhost:1234/ 以查看您的項(xiàng)目。每次保存文件時(shí),瀏覽器中顯示的項(xiàng)目都會(huì)自動(dòng)更新。完成工作會(huì)話后,您可以通過輸入終端 ctrl + C 來停止包裹。
在文件索引中.html ,我們已經(jīng)加載了帶有腳本標(biāo)簽的文件 main.js。因?yàn)槲覀儗⑹褂媚K,所以我們將腳本標(biāo)記的 type 屬性設(shè)置為 module 。JavaScript 模塊的好處是,我們不需要將額外的腳本加載到 index 中.html ;一切都將從主.js.請(qǐng)注意,我們也不需要使用腳本標(biāo)記加載 D3 庫。我們將從下一節(jié)開始安裝和導(dǎo)入所需的 D3 模塊。
為了創(chuàng)建分層可視化,D3 希望我們以特定方式格式化數(shù)據(jù)。我們有兩個(gè)主要選項(xiàng):使用 CSV 文件或使用分層 JSON。
我們的大多數(shù)數(shù)據(jù)都以表格形式出現(xiàn),通常以電子表格的形式出現(xiàn)。此類文件必須通過列指示父子關(guān)系。在表 11.2 中,我們將五種最常用的語言的示例數(shù)據(jù)集重新組織為名為“child”和“parent”的列。稍后我們將使用這些列名稱,讓 D3 知道如何建立父子關(guān)系。在第一行中,子列中有根節(jié)點(diǎn)“語言”。由于這是根節(jié)點(diǎn),因此它沒有父節(jié)點(diǎn)。然后,在下面的行中,我們列出了根的直系子女:印歐語系和漢藏語系。他們都有“語言”作為父母。我們遵循語言分支(日耳曼語、漢尼特語、印度-雅利安語和羅曼語),并聲明哪個(gè)語系是它們的父語言。最后,每種語言(英語、中文普通話、印地語、西班牙語和法語)都有一行,并設(shè)置它們的父語言,即相關(guān)語言分支。我們還為每種語言設(shè)置了“total_speakers”和“native_speakers”列,因?yàn)槲覀兛梢栽诳梢暬惺褂么诵畔ⅲ@些信息對(duì)于分層布局不是必需的。
表 11.2 顯示了在使用 D3 構(gòu)建分層可視化之前我們?nèi)绾螛?gòu)建電子表格。然后,我們將其導(dǎo)出為CSV文件并將其添加到我們的項(xiàng)目中。請(qǐng)注意,您不必為本章的練習(xí)制作自己的電子表格。您可以在 /data 文件夾中找到 100 種最常用的語言(名為 flat_data.csv)格式正確的 CSV 文件。
孩子 | 父母 | 使用 | 母語 |
語言 | |||
印歐語系 | 語言 | ||
漢藏語 | 語言 | ||
日耳曼 | 印歐語系 | ||
西尼特 | 漢藏語 | ||
印度-雅利安語 | 印歐語系 | ||
浪漫 | 印歐語系 | ||
浪漫 | 印歐語系 | ||
英語 | 日耳曼 | 1,132m | 379m |
普通話 | 西尼特 | 1,117m | 918m |
印地語 | 印度-雅利安語 | 615m | 341m |
西班牙語 | 浪漫 | 534m | 460m |
法語 | 浪漫 | 280m | 77m |
讓我們flat_data.csv加載到我們的項(xiàng)目中!首先,在 /js 文件夾中創(chuàng)建一個(gè)新的 JavaScript 文件。將其命名為 load-data.js ,因?yàn)檫@是我們加載數(shù)據(jù)集的地方。在清單 11.1 中,我們創(chuàng)建了一個(gè)名為 loadCSVData() 的函數(shù)。我們向函數(shù)添加導(dǎo)出聲明,使其可供項(xiàng)目中的其他 JavaScript 模塊訪問。
要將 CSV 文件加載到我們的項(xiàng)目中,我們需要采用與使用 d3.csv() 方法不同的路線。宗地需要適當(dāng)?shù)霓D(zhuǎn)換器才能加載 CSV 文件。我們已經(jīng)通過安裝允許 Parcel 解析 CSV 文件的模塊為您完成了項(xiàng)目中的所有配置(有關(guān)更多詳細(xì)信息,請(qǐng)參閱文件 .parcelrc 和 .parcel-transformer-csv.json)。我們現(xiàn)在要做的就是使用 JavaScript require() 函數(shù)加載 CSV 文件并將其保存到常量 csvData 中。如果將 csvData 登錄到控制臺(tái),您將看到它由一個(gè)對(duì)象數(shù)組組成,每個(gè)對(duì)象對(duì)應(yīng)于 CSV 文件中的一行。我們遍歷 csvData 將說話者的數(shù)量格式化為數(shù)字并返回 csvData .
export const loadCSVData=()=> {
const csvData=require("../data/flat_data.csv"); #A
csvData.forEach(d=> { #B
d.total_speakers=+d.total_speakers; #B
d.native_speakers=+d.native_speakers; #B
}); #B
return csvData;
};
在 main.js 中,我們使用導(dǎo)入語句來訪問函數(shù) loadCSVData(),如清單 11.2 所示。然后我們將 loadCSVData() 返回的數(shù)組保存到一個(gè)名為 flatData 的常量中。
import { loadCSVData } from "./load-data.js"; #A
const flatData=loadCSVData(); #B
下一步是將平面 CSV 數(shù)據(jù)轉(zhuǎn)換為分層格式,或包含其子節(jié)點(diǎn)的根節(jié)點(diǎn)。d3-hierarchy 模塊 (https://github.com/d3/d3-hierarchy) 包含一個(gè)名為 d3.stratify() 的方法,它就是這樣做的。它還包括構(gòu)建分層可視化所需的所有其他方法。
為了最大限度地提高項(xiàng)目性能,我們不會(huì)安裝整個(gè) D3 庫,而只會(huì)安裝我們需要的模塊。讓我們從 d3 層次結(jié)構(gòu)開始。在 VS Code 中,打開一個(gè)新的終端窗口并運(yùn)行以下命令:
npm install d3-hierarchy
然后,創(chuàng)建一個(gè)名為 hierarchy 的新 JavaScript 文件.js 。在文件頂部,從 d3-hierarchy 導(dǎo)入 stratify() 方法,如清單 11.3 所示。然后,創(chuàng)建一個(gè)名為 CSVToHierarchy() 的函數(shù),該函數(shù)將 CSV 數(shù)據(jù)作為參數(shù)。請(qǐng)注意,我們通過導(dǎo)出聲明提供此功能。
在 CSVToHierarchy() 中,我們通過調(diào)用方法 stratify() 來聲明一個(gè)層次結(jié)構(gòu)生成器。在我們之前的設(shè)置中,我們會(huì)用 d3.stratify() 調(diào)用此方法。因?yàn)槲覀冎话惭b了 d3-hierarchy 模塊,所以我們不再需要在 d3 對(duì)象上調(diào)用方法并將 stratify() 視為一個(gè)獨(dú)立的函數(shù)。
要將我們的CSV數(shù)據(jù)轉(zhuǎn)換為分層結(jié)構(gòu),函數(shù)stratify()需要知道如何建立父子關(guān)系。使用 id() 訪問器函數(shù),我們指示可以在哪個(gè)鍵下找到子項(xiàng),在本例中為 子項(xiàng)(子項(xiàng)存儲(chǔ)在原始 CSV 文件的“子項(xiàng)”列中)。使用 parentId() 訪問器函數(shù),我們指示可以在哪個(gè)鍵下找到父級(jí),在我們的例子中是父級(jí)(父級(jí)存儲(chǔ)在原始 CSV 文件的“父級(jí)”列中)。
我們將數(shù)據(jù)傳遞給層次結(jié)構(gòu)生成器,并將其保存在一個(gè)名為 root 的常量中,這是我們的分層數(shù)據(jù)結(jié)構(gòu)。這種嵌套數(shù)據(jù)結(jié)構(gòu)帶有一些方法,如 descendants(),它返回樹中所有節(jié)點(diǎn)的數(shù)組(“語言”、“印歐語”、“日耳曼語”、“英語”等),以及 leaves() 返回所有沒有子節(jié)點(diǎn)的數(shù)組(“英語”、“普通話”、“印地語”等)。我們將后代節(jié)點(diǎn)和葉節(jié)點(diǎn)保存到常量中,并使用根數(shù)據(jù)結(jié)構(gòu)返回它們。
import { stratify } from "d3-hierarchy"; #A
export const CSVToHierarchy=(data)=> {
const hierarchyGenerator=stratify() #B
.id(d=> d.child) #B
.parentId(d=> d.parent); #B
const root=hierarchyGenerator(data); #C
const descendants=root.descendants(); #D
const leaves=root.leaves(); #D
return [root, descendants, leaves];
};
在 main.js 中,我們導(dǎo)入函數(shù) CSVToHierarchy() 并調(diào)用它來獲取根、后代和葉。我們將在以下部分中使用此層次結(jié)構(gòu)數(shù)據(jù)結(jié)構(gòu)來生成可視化效果。
import { loadCSVData } from "./load-data.js";
import { CSVToHierarchy } from "./hierarchy.js";
const flatData=loadCSVData();
const [root, descendants, leaves]=CSVToHierarchy(flatData);
我們的數(shù)據(jù)集也可以存儲(chǔ)為分層 JSON 文件。JSON 本質(zhì)上支持分層數(shù)據(jù)結(jié)構(gòu),并使其易于理解。以下 JSON 對(duì)象演示如何為示例數(shù)據(jù)集構(gòu)建數(shù)據(jù)。在文件的根目錄中,我們有一個(gè)用大括號(hào) ( {} ) 括起來的對(duì)象。根的“name”屬性是“語言”,其“子”屬性是一個(gè)對(duì)象數(shù)組。根的每個(gè)直接子級(jí)都是一個(gè)語言家族,其中包含語言分支的“子”數(shù)組,其中還包括帶有語言葉的“子”數(shù)組。請(qǐng)注意,每個(gè)子項(xiàng)都存儲(chǔ)在一個(gè)對(duì)象中。我們可以在葉對(duì)象中添加與語言相關(guān)的其他數(shù)據(jù),例如說話者和母語人士的總數(shù),但這是可選的。
{
"name": "Languages",
"children": [
{
"name": "Indo-European",
"children": [
{
"name": "Germanic",
"children": [
{
"name": "English"
}
]
},
{
"name": "Indo-Aryan",
"children": [
{
"name": "Hindi"
}
]
},
{
"name": "Romance",
"children": [
{
"name": "Spanish"
},
{
"name": "French"
}
]
},
]
},
{
"name": "Sino-Tibetan",
"children": [
{
"name": "Sinitic",
"children": [
{
"name": "Mandarin Chinese"
}
]
}
]
}
]
}
分層 JSON 文件已在數(shù)據(jù)文件夾 ( hierarchical-data.json ) 中可用。我們將以類似的方式處理 CSV 文件以將其加載到我們的項(xiàng)目中。在清單 11.5 中,我們回到 load-data.js 并創(chuàng)建一個(gè)名為 loadJSONData() 的函數(shù)。此函數(shù)使用 JavaScript require() 方法來獲取數(shù)據(jù)集并將其存儲(chǔ)在名為 jsonData 的常量中。常量 jsonData 由函數(shù)返回。
export const loadJSONData=()=> {
const jsonData=require("../data/hierarchical-data.json");
return jsonData;
};
回到main.js,我們導(dǎo)入loadJSONData(),調(diào)用它并將它返回的對(duì)象存儲(chǔ)到一個(gè)名為jsonData的常量中。
import { loadCSVData, loadJSONData } from "./load-data.js";
import { CSVToHierarchy } from "./hierarchy.js";
const flatData=loadCSVData();
const [root, descendants, leaves]=CSVToHierarchy(flatData);
const jsonData=loadJSONData();
為了從 JSON 文件生成分層數(shù)據(jù)結(jié)構(gòu),我們使用方法 d3.hierarchy() 。在示例 11.7 中,我們從 d3-hierarchy 導(dǎo)入層次結(jié)構(gòu)函數(shù)。然后我們創(chuàng)建一個(gè)名為 JSONToHierarchy() 的函數(shù),它將 JSON 數(shù)據(jù)作為參數(shù)。
我們調(diào)用 hierarchy() 函數(shù)并將數(shù)據(jù)作為參數(shù)傳遞。我們將它返回的嵌套數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)在名為 root 的常量中。與之前由 stratify() 函數(shù)返回的數(shù)據(jù)結(jié)構(gòu)一樣,root 有一個(gè)方法后代 (),它返回樹中所有節(jié)點(diǎn)的數(shù)組(“語言”、“印歐語”、“日耳曼語”、“英語”等),還有一個(gè)方法 leaves() 返回所有沒有子節(jié)點(diǎn)的數(shù)組(“英語”、“普通話”、“印地語”等)。我們將后代節(jié)點(diǎn)和葉節(jié)點(diǎn)保存到常量中,并使用根數(shù)據(jù)結(jié)構(gòu)返回它們。
import { stratify, hierarchy } from "d3-hierarchy";
...
export const JSONToHierarchy=(data)=> {
const root=hierarchy(data);
const descendants=root.descendants();
const leaves=root.leaves();
return [root, descendants, leaves];
};
最后,在main.js中,我們導(dǎo)入根,后代和葉子數(shù)據(jù)結(jié)構(gòu)。為了將它們與從 CSV 數(shù)據(jù)導(dǎo)入的后綴區(qū)分開來,我們添加了_j后綴。
import { loadCSVData, loadJSONData } from "./load-data.js";
import { CSVToHierarchy, JSONToHierarchy } from "./hierarchy.js";
const flatData=loadCSVData();
const [root, descendants, leaves]=CSVToHierarchy(flatData);
const jsonData=loadJSONData();
const [root_j, descendants_j, leaves_j]=JSONToHierarchy(jsonData);
在現(xiàn)實(shí)生活中的項(xiàng)目中,我們不需要同時(shí)加載CSV和JSON數(shù)據(jù);這將是一個(gè)或另一個(gè)。我們這樣做只是出于教學(xué)目的。
有兩種主要方法可以將分層數(shù)據(jù)加載到 D3 項(xiàng)目中:從 CSV 文件或分層 JSON 文件。如圖 11.3 所示,如果我們使用 CSV 文件中的數(shù)據(jù),我們會(huì)將其傳遞給 d3.stratify() 以生成層次結(jié)構(gòu)數(shù)據(jù)結(jié)構(gòu)。如果我們使用 JSON 文件,我們將使用 d3.hierarchy() 方法代替。這兩種方法都返回相同的嵌套數(shù)據(jù)結(jié)構(gòu),通常稱為 root 。此根有一個(gè)返回層次結(jié)構(gòu)中所有節(jié)點(diǎn)的方法 descendants() 和一個(gè)返回沒有子節(jié)點(diǎn)的方法 leaves()。
現(xiàn)在我們的分層數(shù)據(jù)已經(jīng)準(zhǔn)備就緒,我們可以進(jìn)入有趣的部分并構(gòu)建可視化!這就是我們將在以下部分中執(zhí)行的操作。
在圓形包中,我們用圓圈表示每個(gè)節(jié)點(diǎn),子節(jié)點(diǎn)嵌套在其父節(jié)點(diǎn)中。當(dāng)我們想要一目了然地理解整個(gè)分層組織時(shí),這種可視化很有幫助。它易于理解,外觀令人愉悅。
在本節(jié)中,我們將使用圖 100.11 所示的圓形包可視化我們的 4 種最常用的語言數(shù)據(jù)集。在此可視化中,最外層的圓圈是根節(jié)點(diǎn),我們將其命名為“語言”。顏色較深的圓圈是語系,顏色較淺的圓圈是語言分支。白色圓圈表示語言,其大小表示說話者的數(shù)量。
要使用 D3 創(chuàng)建圓形包裝可視化,我們需要使用布局生成器。與第5章中討論的類似,此類生成器將現(xiàn)有數(shù)據(jù)集附加構(gòu)建圖表所需的信息,例如每個(gè)圓和節(jié)點(diǎn)的位置和半徑,如圖11.5所示。
我們已經(jīng)有了名為 root 的分層數(shù)據(jù)結(jié)構(gòu),并準(zhǔn)備跳到圖 11.5 中的第二步。首先,讓我們創(chuàng)建一個(gè)名為 circle-pack 的新 JavaScript 文件.js并聲明一個(gè)名為 drawCirclePack() 的函數(shù)。此函數(shù)采用根、后代,并將上一節(jié)中創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)保留為參數(shù)。
export const drawCirclePack=(root, descendants, leaves)=> {};
在main.js中,我們導(dǎo)入drawCirclePack()并調(diào)用它,將根,后代和葉作為參數(shù)傳遞。
import { drawCirclePack } from "./circle-pack.js";
drawCirclePack(root, descendants, leaves);
回到 circle-pack.js ,在函數(shù) drawCirclePack() 中,我們將開始計(jì)算我們的布局。在示例 11.9 中,我們首先聲明圖表的維度。我們將寬度和高度都設(shè)置為 800px。然后,我們聲明一個(gè)邊距對(duì)象,其中上邊距、右邊距、下邊距和左邊距等于 1px。我們需要此邊距才能看到可視化的最外層圓圈。最后,我們使用第 4 章中采用的策略計(jì)算圖表的內(nèi)部寬度和高度。
然后,我們調(diào)用 sum() 方法,該方法可用于 root。此方法負(fù)責(zé)計(jì)算可視化效果的聚合大小。我們還向 D3 指示應(yīng)從中計(jì)算葉節(jié)點(diǎn)半徑的鍵:total_speakers 。
為了初始化包布局生成器,我們調(diào)用 D3 方法 pack() ,我們從文件頂部的 d3-hierarchy 導(dǎo)入該方法。我們使用它的 size() 訪問函數(shù)來設(shè)置圓形包的整體大小,并使用 padding() 函數(shù)將圓圈之間的空間設(shè)置為 3px。
import { pack } from "d3-hierarchy";
export const drawCirclePack=(root, descendants, leaves)=> {
const width=800; #A
const height=800; #A
const margin={ top: 1, right: 1, bottom: 1, left: 1 }; #A
const innerWidth=width - margin.right - margin.left; #A
const innerHeight=height - margin.top - margin.bottom; #A
root.sum(d=> d.total_speakers); #B
const packLayoutGenerator=pack() #C
.size([innerWidth, innerHeight]) #C
.padding(3); #C
packLayoutGenerator(root); #D
};
如果將后代數(shù)組記錄到控制臺(tái)中,您將看到包布局生成器為每個(gè)節(jié)點(diǎn)追加了以下信息:
我們將在下一節(jié)中使用此信息來繪制圓形包。
我們現(xiàn)在準(zhǔn)備繪制我們的圓形包!要選擇元素并將其附加到 DOM,我們需要通過在終端中運(yùn)行 npm install d3-select 來安裝 d3 選擇模塊 (https://github.com/d3/d3-selection)。此模塊包含負(fù)責(zé)操作 DOM、應(yīng)用數(shù)據(jù)綁定模式和偵聽事件的 D3 方法。在 circle-pack 的頂部.js ,我們從 d3-select 導(dǎo)入 select() 函數(shù)。
在 drawCirclePack() 中,我們將一個(gè) SVG 容器附加到 div 中,其 id 為 “circle-pack”,該容器已存在于索引中.html 。我們按照第 4 章中解釋的策略設(shè)置其 viewBox 屬性并附加一個(gè)組以包含內(nèi)部圖表。
然后,我們?yōu)楹蟠鷶?shù)組中的每個(gè)節(jié)點(diǎn)附加一個(gè)圓圈。我們使用包布局生成器附加到數(shù)據(jù)的 x 和 y 值來設(shè)置它們的 cx 和 cy 屬性。我們對(duì)半徑做同樣的事情。現(xiàn)在,我們將圓圈的填充屬性設(shè)置為“透明”,將其筆觸設(shè)置為“黑色”。我們稍后會(huì)改變這一點(diǎn)。
import { pack } from "d3-hierarchy";
import { select } from "d3-selection";
export const drawCirclePack=(root, descendants, leaves)=> {
...
const svg=select("#circle-pack") #A
.append("svg") #A
.attr("viewBox", `0 0 ${width} ${height}`) #A
.append("g") #A
.attr("transform", `translate(${margin.left}, ${margin.top})`); #A
svg #B
.selectAll(".pack-circle") #B
.data(descendants) #B
.join("circle") #B
.attr("class", "pack-circle") #B
.attr("cx", d=> d.x) #C
.attr("cy", d=> d.y) #C
.attr("r", d=> d.r) #C
.attr("fill", "none")
.attr("stroke", "black");
};
完成此步驟后,您的圓形包應(yīng)如圖 11.6 所示。我們的可視化正在形成!
我們希望圓圈包中的每個(gè)語言家族都有自己的顏色。如果打開文件幫助程序.js ,您將看到一個(gè)名為 languageFamilies 的數(shù)組。它包含語言系列及其相關(guān)顏色的列表,如以下代碼片段所示。我們可以使用此數(shù)組來創(chuàng)建色階并使用它來設(shè)置每個(gè)圓的填充屬性。
export const languageFamilies=[
{ label: "Indo-European", color: "#4E86A5" },
{ label: "Sino-Tibetan", color: "#9E4E9E" },
{ label: "Afro-Asiatic", color: "#59C8DC" },
{ label: "Austronesian", color: "#3E527B" },
{ label: "Japanic", color: "#F99E23" },
{ label: "Niger-Congo", color: "#F36F5E" },
{ label: "Dravidian", color: "#C33D54" },
{ label: "Turkic", color: "#D57AB1" },
{ label: "Koreanic", color: "#33936F" },
{ label: "Kra-Dai", color: "#36311F" },
{ label: "Uralic", color: "#B59930" },
];
要使用 D3 縮放,我們需要安裝 d3 縮放模塊 (https://github.com/d3/d3-scale) npm 安裝 d3-scale 。對(duì)于我們的色階,我們將使用序數(shù)刻度,它采用離散數(shù)組作為域、語言系列,將離散數(shù)組作為范圍,即關(guān)聯(lián)的顏色。在示例 11.11 中,我們創(chuàng)建了一個(gè)名為 scales.js 的新文件。在文件的頂部,我們從 d3-scale 導(dǎo)入 scaleOrdinal,從 helper.js 導(dǎo)入我們的 languageFamilies 數(shù)組。然后,我們聲明一個(gè)名為 colorScale 的序數(shù)刻度,傳遞一個(gè)語言家族標(biāo)簽數(shù)組作為域,傳遞一個(gè)關(guān)聯(lián)顏色數(shù)組作為范圍。我們使用 JavaScript map() 方法生成這些數(shù)組。
import { scaleOrdinal } from "d3-scale";
import { languageFamilies } from "./helper";
export const colorScale=scaleOrdinal()
.domain(languageFamilies.map(d=> d.label))
.range(languageFamilies.map(d=> d.color));
在第 11.2.1 節(jié)結(jié)束時(shí),我們討論了 D3 包布局生成器如何將多條信息附加到后代數(shù)據(jù)集(也稱為節(jié)點(diǎn)),包括它們的深度。圖 11.7 顯示我們的圓形包的深度從 2 到 3 不等。表示“語言”根節(jié)點(diǎn)的最外層圓的深度為零。此圓圈具有灰色邊框和透明填充。以下圓圈是深度為一的語言家族。它們的 fill 屬性對(duì)應(yīng)于我們剛剛聲明的色階返回的顏色。然后,語言分支的深度為 <>。他們繼承了父母顏色的更蒼白版本。最后,葉節(jié)點(diǎn)或語言的深度為 <>,顏色為白色。這種顏色漸變不遵循任何特定規(guī)則,但旨在使父子關(guān)系盡可能明確。
回到 circle-pack.js ,我們將使用色階設(shè)置圓圈的填充屬性。在文件的頂部,我們導(dǎo)入之前以比例創(chuàng)建的色階.js .為了生成語言分支的較淺顏色(深度為 2 的圓圈),我們將使用稱為插值的 d3 方法,該方法在 d3-插值模塊 (https://github.com/d3/d3-interpolate) 中可用。使用 npm 安裝 d3 插值安裝此模塊,并將此方法導(dǎo)入到圓包的頂部.js .
在示例 11.12 中,我們回到設(shè)置圓圈填充屬性的代碼。我們使用 JavaScript switch() 語句來評(píng)估附加到每個(gè)節(jié)點(diǎn)的深度數(shù)的值。如果深度為 3,則節(jié)點(diǎn)是一個(gè)語言家族。我們將它的 id 傳遞給色標(biāo),色標(biāo)返回關(guān)聯(lián)的顏色。對(duì)于語言分支,我們?nèi)匀徽{(diào)用色階,但在其父節(jié)點(diǎn)的值上(d.parent.id)。然后,我們將比例返回的顏色作為 d0 插值() 函數(shù)的第一個(gè)參數(shù)傳遞。第二個(gè)參數(shù)是“白色”,即我們想要插入初始值的顏色。我們還將值 5.50 傳遞給 interpolate() 函數(shù),以指示我們想要一個(gè)介于原始顏色和白色之間的 <>% 的值。最后,我們?yōu)樗惺S喙?jié)點(diǎn)返回默認(rèn)填充屬性“white”。
我們還更改了圓圈的筆觸屬性。如果深度為零,因此節(jié)點(diǎn)是最外層的圓,我們給它一個(gè)灰色的筆觸。否則,不應(yīng)用筆畫。
...
import { colorScale } from "./scales";
import { interpolate } from "d3-interpolate";
export const drawCirclePack=(root, descendants, leaves)=> {
...
svg
.selectAll(".pack-circle")
.data(descendants)
.join("circle")
.attr("class", "pack-circle")
...
.attr("fill", d=> {
switch (d.depth) { #A
case 1: #B
return colorScale(d.id); #B
case 2: #C
return interpolate(colorScale(d.parent.id), "white")(0.5); #C
default: #D
return "white"; #D
};
})
.attr("stroke", d=> d.depth===0 ? "grey" : "none"); #E
};
完成后,您的彩色圓圈包應(yīng)如圖 11.8 所示。
我們的圓圈包絕對(duì)看起來不錯(cuò),但沒有提供任何關(guān)于哪個(gè)圓圈代表哪種語言、分支或家族的線索。圓形包裝的主要缺點(diǎn)之一是在保持其可讀性的同時(shí)在其上貼標(biāo)簽并不容易。但是由于我們正在從事數(shù)字項(xiàng)目,因此我們可以通過鼠標(biāo)交互向讀者提供其他信息。
在本節(jié)中,我們將首先為較大的語言圈添加標(biāo)簽。然后,我們將構(gòu)建一個(gè)交互式工具,當(dāng)鼠標(biāo)位于葉節(jié)點(diǎn)上時(shí),該工具可提供其他信息。
在我們的可視化中,我們處理的是名稱相對(duì)較短的語言,如法語或德語,以及其他名稱較長的語言,如“現(xiàn)代標(biāo)準(zhǔn)阿拉伯語”或“西旁遮普語”。要在相應(yīng)的圓圈內(nèi)顯示這些較長的標(biāo)簽,我們需要讓它們分成多行。但是,如果您還記得我們之前關(guān)于 SVG 文本元素的討論,則可以在多行上顯示它們,但需要大量工作。使用常規(guī) HTML 文本在需要時(shí)自動(dòng)換行,這要容易得多!猜猜看:我們可以在 SVG 元素中使用常規(guī) HTML 元素,這正是我們?cè)谶@里要做的。
SVG 元素 foreignObject 允許我們?cè)?SVG 容器中包含常規(guī) HTML 元素,例如 div 。然后這個(gè)div可以像任何其他div一樣設(shè)置樣式,并且它的文本將在需要時(shí)自動(dòng)換行。
在 D3 中,我們附加 foreignObject 元素的方式與其他任何元素相同。然后,在這些外來對(duì)象元素中,我們附加我們需要的 div。您可以將這些視為SVG和HTML世界之間的網(wǎng)關(guān)。
出于可讀性的目的,我們不會(huì)在每個(gè)語言圈上應(yīng)用標(biāo)簽,而只會(huì)在較大的語言圈上應(yīng)用標(biāo)簽。在示例 11.13 中,我們首先定義要應(yīng)用標(biāo)簽的圓的最小半徑,即 22px。然后,我們使用數(shù)據(jù)綁定模式為每個(gè)滿足最小半徑要求的葉節(jié)點(diǎn)附加一個(gè) foreignObject 元素。外來對(duì)象元素有四個(gè)必需的屬性:
然后,我們需要指定要附加到 foreignObject 中的元素的 XML 命名空間。這就是為什么我們附加一個(gè) xhtml:div 而不僅僅是一個(gè) div .我們給這個(gè)div一個(gè)類名“l(fā)eaf-label”,并將其文本設(shè)置為節(jié)點(diǎn)的id。文件可視化.css 已包含在 foreignObject 元素內(nèi)水平和垂直居中標(biāo)簽所需的樣式。
export const drawCirclePack=(root, descendants, leaves)=> {
...
const minRadius=22;
svg
.selectAll(".leaf-label-container") #A
.data(leaves.filter(leave=> leave.r >=minRadius)) #A
.join("foreignObject") #A
.attr("class", "leaf-label-container")
.attr("width", d=> 2 * d.r) #B
.attr("height", 40) #B
.attr("x", d=> d.x - d.r) #B
.attr("y", d=> d.y - 20) #B
.append("xhtml:div") #C
.attr("class", "leaf-label") #C
.text(d=> d.id); #C
};
應(yīng)用標(biāo)簽后,您的圓形包應(yīng)如圖 11.4 和托管項(xiàng)目 (https://d3js-in-action-third-edition.github.io/100-most-spoken-languages/) 中的包所示。現(xiàn)在,我們可以在可視化中找到主要語言。
在第 7 章中,我們討論了如何使用 D3 偵聽鼠標(biāo)事件,例如顯示工具提示。在本節(jié)中,我們將構(gòu)建類似的東西,但不是在可視化效果上顯示工具提示,而是將其移動(dòng)到一側(cè)。只要您有超過幾行信息要向用戶顯示,這是一個(gè)很好的選擇。由于此類工具提示是使用 HTML 元素構(gòu)建的,因此也更容易設(shè)置樣式。
在文件索引中.html ,取消注釋 id 為“信息容器”的 div。此 div 包含兩個(gè)主要元素:
在示例 11.14 中,我們又回到了 circle-pack.js 。在文件頂部,我們從 d3-select 導(dǎo)入 selectAll 函數(shù)。我們還需要安裝 d3 格式模塊 (https://github.com/d3/d3-format) 并導(dǎo)入其格式函數(shù)。
為了區(qū)分節(jié)點(diǎn)級(jí)別,在清單 11.14 中,我們將它們的深度值添加到它們的類名中。然后,我們使用 selectAll() 函數(shù)選擇所有類名為 “pack-circle-depth-3” 的圓圈和所有 foreignObject 元素。我們使用 D3 on() 方法將 mouseenter 事件偵聽器附加到葉節(jié)點(diǎn)及其標(biāo)簽。在此事件偵聽器的回調(diào)函數(shù)中,我們使用附加到元素的數(shù)據(jù)來填充有關(guān)相應(yīng)語言、分支、家族和說話人數(shù)量的工具提示信息。請(qǐng)注意,我們使用 format() 函數(shù)來顯示具有三個(gè)有效數(shù)字和后綴的揚(yáng)聲器數(shù)量,例如“M”表示“百萬”(“.3s”);
然后,我們通過添加和刪除類名“hidden”來隱藏說明并顯示工具提示。我們還在鼠標(biāo)離開語言節(jié)點(diǎn)或其標(biāo)簽時(shí)應(yīng)用事件偵聽器。在其回調(diào)函數(shù)中,我們隱藏工具提示并顯示說明。
import { select, selectAll } from "d3-selection";
import { format } from "d3-format";
export const drawCirclePack=(root, descendants, leaves)=> {
...
svg
.selectAll(".pack-circle")
.data(descendants)
.join("circle")
.attr("class", d=> `pack-circle pack-circle-depth-${d.depth}`) #A
...
selectAll(".pack-circle-depth-3, foreignObject") #B
.on("mouseenter", (e, d)=> { #C
select("#info .info-language").text(d.id); #D
select("#info .info-branch .information").text(d.parent.id); #D
select("#info .info-family .information") #D
? .text(d.parent.data.parent); #D
select("#info .info-total-speakers .information") #D
? .text(format(".3s")(d.data.total_speakers)); #D
select("#info .info-native-speakers .information") #D
? .text(format(".3s")(d.data.native_speakers)); #D
select("#instructions").classed("hidden", true); #E
select("#info").classed("hidden", false); #E
})
.on("mouseleave", ()=> { #F
select("#instructions").classed("hidden", false); #G
select("#info").classed("hidden", true); #G
});
};
當(dāng)您將鼠標(biāo)移到語言節(jié)點(diǎn)上時(shí),您現(xiàn)在應(yīng)該會(huì)看到有關(guān)分支、系列和說話人數(shù)量的其他信息顯示在可視化效果的右側(cè),如圖 11.9 所示。
圓形包的一個(gè)缺點(diǎn)是它們很難在移動(dòng)屏幕上呈現(xiàn)。盡管圓圈仍然在小屏幕上提供了父子關(guān)系的良好概述,但標(biāo)簽變得更加難以閱讀。此外,由于語言圈可能會(huì)變小,因此使用觸摸事件顯示信息可能會(huì)很棘手。為了解決這些缺點(diǎn),我們可以將語言家族相互堆疊,或者在移動(dòng)設(shè)備上選擇不同類型的可視化。
可視化父子關(guān)系的一種熟悉且直觀的方法是使用樹形圖。樹形圖類似于家譜樹。像圓形包一樣,它們由節(jié)點(diǎn)組成,但也顯示了它們之間的鏈接。在本節(jié)中,我們將構(gòu)建 100 種最常用的語言的樹形圖,如圖 11.10 所示。左側(cè)是根節(jié)點(diǎn),即“語言”。它分為語系,也細(xì)分為語言分支,最后是語言。我們用圓圈的大小可視化每種語言的使用者總數(shù)。至于圓圈包,這些圓圈的顏色代表它們所屬的語言家族。
與上一節(jié)中構(gòu)建的圓形包類似,D3樹形圖是使用布局生成器d3.tree()創(chuàng)建的,它是d3層次結(jié)構(gòu)模塊(https://github.com/d3/d3-hierarchy)的一部分。然后,我們使用布局提供的信息來繪制鏈接和節(jié)點(diǎn)。
要生成樹布局,讓我們首先創(chuàng)建一個(gè)新文件并將其命名為 tree.js .在這個(gè)文件中,我們創(chuàng)建了一個(gè)名為drawTree()的函數(shù),它將分層數(shù)據(jù)(也稱為根,后代和葉)作為參數(shù)。在示例 11.15 中,我們聲明了圖表的維度。我們給它一個(gè) 1200px 的寬度,圖表的 HTML 容器的寬度,以及 3000px 的高度。請(qǐng)注意,高度與圖表中的葉節(jié)點(diǎn)數(shù)成正比,并且是通過反復(fù)試驗(yàn)找到的。處理樹可視化時(shí),請(qǐng)從大致值開始,并在可視化顯示在屏幕上后進(jìn)行調(diào)整。
為了生成布局,我們調(diào)用 D3 的 tree() 函數(shù),我們從文件頂部的 d3-hierarchy 導(dǎo)入該函數(shù),并設(shè)置其 size() 訪問器函數(shù),該函數(shù)將圖表的寬度和高度數(shù)組作為參數(shù)。因?yàn)槲覀兿M覀兊臉鋸淖蟮接艺归_,所以我們首先傳遞 innerHeight,然后是 innerWidth 。如果我們希望樹從上到下部署,我們會(huì)做相反的事情。最后,我們將分層數(shù)據(jù)(根)傳遞給樹布局生成器。
import { tree } from "d3-hierarchy";
export const drawTree=(root, descendants, leaves)=> {
const width=1200; #A
const height=3000; #A
const margin={top:60, right: 200, bottom: 0, left: 100}; #A
const innerWidth=width - margin.left - margin.right; #A
const innerHeight=height - margin.top - margin.bottom; #A
const treeLayoutGenerator=tree() #B
.size([innerHeight, innerWidth]); #B
treeLayoutGenerator(root); #C
};
在main.js中,我們還需要導(dǎo)入drawTree()函數(shù)并將根、后代和葉作為參數(shù)傳遞。
import { drawTree } from "./tree.js";
drawTree(root, descendants, leaves);
生成布局后,繪制樹形圖非常簡單。像往常一樣,我們首先需要附加一個(gè) SVG 容器并設(shè)置其 viewBox 屬性。在示例 11.16 中,我們將這個(gè)容器附加到 div 中,其 id 為 “tree”,該 id 已存在于 index.html 中。請(qǐng)注意,我們必須從文件頂部的 d3-select 模塊導(dǎo)入 select() 函數(shù)。我們還將一個(gè) SVG 組附加到此容器,并根據(jù)前面定義的左邊距和上邊距進(jìn)行轉(zhuǎn)換,遵循自第 4 章以來使用的策略。
要?jiǎng)?chuàng)建鏈接,我們需要 d3.link() 鏈接生成器函數(shù)。此函數(shù)的工作方式與第 3 章中介紹的線路生成器完全相同。它是 d3 形狀模塊 (https://github.com/d3/d3-shape) 的一部分,我們使用命令安裝它 npm 安裝 d3 形狀 .在文件的頂部,我們從 d3-shape 導(dǎo)入 link() 函數(shù),以及 curveBumpX() 函數(shù),我們將使用它來確定鏈接的形狀。
然后我們聲明一個(gè)名為 linkGenerator 的鏈接生成器,它將曲線函數(shù) curveBumpX 傳遞給 D3 的 link() 函數(shù)。我們將它的 x() 和 y() 訪問器函數(shù)設(shè)置為使用樹布局生成器存儲(chǔ)在 y 和 x 鍵中的值。就像我們準(zhǔn)備樹布局生成器時(shí)一樣,x 和 y 值是反轉(zhuǎn)的,因?yàn)槲覀兿M麖挠业阶蠖皇菑纳系较吕L制樹。
為了繪制鏈接,我們使用數(shù)據(jù)綁定模式從 root.links() 提供的數(shù)據(jù)中附加路徑元素。此方法返回樹的鏈接數(shù)組及其源點(diǎn)和目標(biāo)點(diǎn)。然后調(diào)用鏈接生成器來計(jì)算每個(gè)鏈接或路徑的 d 屬性。最后,我們?cè)O(shè)置鏈接的樣式并將其不透明度設(shè)置為 60%。
...
import { select } from "d3-selection";
import { link, curveBumpX } from "d3-shape";
export const drawTree=(root, descendants)=> {
...
const svg=select("#tree") #A
.append("svg") #A
.attr("viewBox", `0 0 ${width} ${height}`) #A
.append("g") #A
.attr("transform", `translate(${margin.left}, ${margin.top})`); #A
const linkGenerator=link(curveBumpX) #B
.x(d=> d.y) #B
.y(d=> d.x); #B
svg #C
.selectAll(".tree-link") #C
.data(root.links()) #C
.join("path") #C
.attr("class", "tree-link") #C
.attr("d", d=> linkGenerator(d)) #C
.attr("fill", "none") #C
.attr("stroke", "grey") #C
.attr("stroke-opacity", 0.6); #C
};
準(zhǔn)備就緒后,您的鏈接將類似于圖 11.12 中的鏈接。請(qǐng)注意,此圖僅顯示部分視圖,因?yàn)槲覀兊臉浞浅8撸?/span>
為了突出顯示每個(gè)節(jié)點(diǎn)的位置,我們將在樹形圖中附加圓圈。帶有灰色筆劃的小圓圈將表示根、語言家族和語言分支節(jié)點(diǎn)。相反,語言節(jié)點(diǎn)圓圈的大小與說話者總數(shù)成比例,并且具有與其語言家族關(guān)聯(lián)的顏色。
要計(jì)算語言節(jié)點(diǎn)的大小,我們需要一個(gè)規(guī)模。在清單 11.17 中,我們轉(zhuǎn)到 scales.js并從 d3-scale 導(dǎo)入 scaleRadial()。量表的域是連續(xù)的,從零擴(kuò)展到數(shù)據(jù)集中說其中一種語言的最大人數(shù)。它的范圍可以在 83 到 <>px 之間變化,這是上一節(jié)中創(chuàng)建的圓包中最大圓的半徑。
因?yàn)樽畲笳f話人數(shù)只有在我們檢索數(shù)據(jù)并創(chuàng)建層次結(jié)構(gòu)(根)后才可用,我們需要將徑向刻度包裝到一個(gè)名為 getRadius() 的函數(shù)中。當(dāng)我們需要計(jì)算圓的半徑時(shí),我們將傳遞當(dāng)前語言的說話者數(shù)量以及最大說話者數(shù)量,此函數(shù)將返回半徑。
import { scaleOrdinal, scaleRadial } from "d3-scale";
...
export const getRadius=(maxSpeakers, speakers)=> {
const radialScale=scaleRadial()
.domain([0, maxSpeakers])
.range([0, 83]);
return radialScale(speakers);
};
回到樹.js ,我們用方法 d3.max() 計(jì)算最大揚(yáng)聲器數(shù)。要使用這種方法,我們需要安裝 d3-array 模塊 (https://github.com/d3/d3-array) 與 npm install d3-array ,并在文件頂部導(dǎo)入 max() 函數(shù)。我們還從 scales 導(dǎo)入函數(shù) getRadius() 和色階.js .
然后,我們使用數(shù)據(jù)綁定模式將一個(gè)圓附加到每個(gè)后代節(jié)點(diǎn)的內(nèi)部圖表中。我們使用樹布局生成器附加到數(shù)據(jù)中的 x 和 y 鍵來設(shè)置這些圓圈的 cx 和 cy 屬性。如果圓是一個(gè)葉節(jié)點(diǎn),我們根據(jù)相關(guān)語言的說話者數(shù)量和 getRadius() 函數(shù)設(shè)置其半徑。我們使用色階設(shè)置其顏色,填充不透明度為 30%,描邊設(shè)置為“無”。其他圓圈的半徑為 4px,白色填充和灰色描邊。
...
import { max } from "d3-array";
import { getRadius, colorScale } from "./scales";
export const drawTree=(root, descendants)=> {
...
const maxSpeakers=max(leaves, d=> d.data.total_speakers); #A
svg #B
.selectAll(".node-tree") #B
.data(descendants) #B
.join("circle") #B
.attr("class", "node-tree") #B
.attr("cx", d=> d.y) #B
.attr("cy", d=> d.x) #B
.attr("r", d=> d.depth===3 #C
? getRadius(maxSpeakers, d.data.total_speakers) #C
: 4 #C
) #C
.attr("fill", d=> d.depth===3 #D
? colorScale(d.parent.data.parent) #D
: "white" #D
) #D
.attr("fill-opacity", d=> d.depth===3 #E
? 0.3 #E
: 1 #E
) #E
.attr("stroke", d=> d.depth===3 #E
? "none" #E
: "grey" #E
); #E
};
為了完成樹形圖,我們?yōu)槊總€(gè)節(jié)點(diǎn)添加一個(gè)標(biāo)簽。在示例 11.19 中,我們使用數(shù)據(jù)綁定模式為數(shù)據(jù)集中的每個(gè)節(jié)點(diǎn)附加一個(gè)文本元素。如果標(biāo)簽與葉節(jié)點(diǎn)相關(guān)聯(lián),則在右側(cè)顯示標(biāo)簽。否則,標(biāo)簽將位于其節(jié)點(diǎn)的左側(cè)。我們還為標(biāo)簽提供白色筆觸,以便它們?cè)诜胖迷阪溄由蠒r(shí)更易于閱讀。通過將繪制順序?qū)傩栽O(shè)置為“描邊”,我們可以確保在文本填充顏色之前繪制描邊。這也有助于提高可讀性。
export const drawTree=(root, descendants)=> {
...
svg
.selectAll(".label-tree") #A
.data(descendants) #A
.join("text") #A
.attr("class", "label-tree") #A
.attr("x", d=> d.children ? d.y - 8 : d.y + 8) #B
.attr("y", d=> d.x) #B
.attr("text-anchor", d=> d.children ? "end" : "start") #B
.attr("alignment-baseline", "middle") #B
.attr("paint-order", "stroke") #C
.attr("stroke", d=> d.depth===3 ? "none" : "white") #C
.attr("stroke-width", 2) #C
.style("font-size", "16px")
.text(d=> d.id);
};
完成后,您的樹形圖應(yīng)類似于托管項(xiàng)目 (https://d3js-in-action-third-edition.github.io/100-most-spoken-languages/) 和圖 11.13 中的樹形圖。
此樹形圖的線性布局使其相對(duì)容易地轉(zhuǎn)換為移動(dòng)屏幕,只要我們?cè)黾訕?biāo)簽的字體大小并確保它們之間有足夠的垂直空間。有關(guān)構(gòu)建響應(yīng)式圖表的提示,請(qǐng)參閱第 9 章。
為了完成這個(gè)項(xiàng)目,我們需要為語言家族的顏色和圓圈的大小添加一個(gè)圖例。我們已經(jīng)為您構(gòu)建了它。要顯示圖例,請(qǐng)轉(zhuǎn)到索引.html然后取消注釋帶有“圖例”類的 div。然后,在main.js中,從legend導(dǎo)入函數(shù)createLegend(.js并調(diào)用它來生成legend。請(qǐng)參閱第7章中的鯨目動(dòng)物可視化,以獲取有關(guān)我們?nèi)绾螛?gòu)建這個(gè)傳說的更多解釋。看看圖例中的代碼.js甚至更好的是,嘗試自己構(gòu)建它!
在本章中,我們討論了如何構(gòu)建圓形包和樹形圖可視化。使用 D3 制作其他層次結(jié)構(gòu)表示形式(如樹狀圖和冰柱圖)非常相似。
圖 11.14 說明了我們?nèi)绾螐?CSV 或 JSON 數(shù)據(jù)開始,我們將其格式化為名為 root 的分層數(shù)據(jù)結(jié)構(gòu)。使用這種數(shù)據(jù)結(jié)構(gòu),我們可以構(gòu)建任何分層可視化,唯一的區(qū)別是用作布局生成器的功能。對(duì)于圓形包,布局生成器函數(shù)為 d3.pack() ;對(duì)于樹狀圖,d3.treemap() ;對(duì)于樹形圖,d3.tree() ;對(duì)于冰柱圖,d3.partition() 。我們可以在 d3 層次結(jié)構(gòu)模塊 (https://github.com/d3/d3-hierarchy) 中找到這些布局生成器。
現(xiàn)在,您已經(jīng)掌握了構(gòu)建 100 種最常用的語言的樹狀圖所需的所有知識(shí),如下圖所示,以及托管項(xiàng)目 (https://d3js-in-action-third-edition.github.io/100-most-spoken-languages/)。樹狀圖將分層數(shù)據(jù)可視化為一組嵌套矩形。傳統(tǒng)上,樹狀圖僅顯示葉節(jié)點(diǎn),在我們的例子中,葉節(jié)點(diǎn)是語言。矩形或葉節(jié)點(diǎn)的大小與每種語言的使用者總數(shù)成正比。
1. 在索引中.html添加一個(gè) id 為“樹狀圖”的 div。
2. 使用一個(gè)名為 drawTreemap() 的函數(shù)創(chuàng)建一個(gè)名為 treemap.js 的新文件。此函數(shù)接收根數(shù)據(jù)結(jié)構(gòu)和葉作為參數(shù),并從 main.js 調(diào)用。
3. 使用 d3.treemap() 布局生成器計(jì)算樹狀圖布局。使用 size() 訪問器函數(shù),設(shè)置圖表的寬度和高度。您還可以使用填充Inner()和paddingOuter()指定矩形之間的填充。有關(guān)更深入的文檔 (https://github.com/d3/d3-hierarchy),請(qǐng)參閱 d3 層次結(jié)構(gòu)模塊。
4. 將 SVG 容器附加到 div,ID 為“樹狀圖”。
5. 為每個(gè)葉節(jié)點(diǎn)附加一個(gè)矩形。使用布局生成器添加到數(shù)據(jù)集的信息設(shè)置其位置和大小。
6. 在相應(yīng)的矩形上附加每種語言的標(biāo)簽。您可能還希望隱藏顯示在較小矩形上的標(biāo)簽。
如果您在任何時(shí)候遇到困難或想將您的解決方案與我們的解決方案進(jìn)行比較,您可以在附錄 D 的 D.11 節(jié)和文件夾 11.4-樹狀圖/本章代碼文件的末尾找到它。但是,像往常一樣,我們鼓勵(lì)您嘗試自己完成它。您的解決方案可能與我們的略有不同,沒關(guān)系!
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。