Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
哥爬蟲
大數據時代,各行各業對數據采集的需求日益增多,網絡爬蟲的運用也更為廣泛,越來越多的人開始學習網絡爬蟲這項技術,K哥爬蟲此前已經推出不少爬蟲進階、逆向相關文章,為實現從易到難全方位覆蓋,特設【0基礎學爬蟲】專欄,幫助小白快速入門爬蟲,本期為網頁基本結構介紹。
網頁是互聯網應用的一種形態,是組成網站的基本元素。它是一個包含HTML標簽的純文本文件,可以存放在世界上任意一臺計算機中。網頁可以被看作為承載各種網站應用和信息的容器,網站的可視化信息都通過網頁來進行展示,為網站用戶提供一個友好的界面。
表面上,網頁的組成可以分為文字、圖片、音頻、視頻、超鏈接等元素構成,這些元素是用戶能夠直接看到的。但在本質上,網頁的組成分為三部分:
HTML的全稱為超文本標記語言,是一種標記語言,它是標準通用標記語言下的一個應用,也是一種規范,一種標準,它通過標記符號來標記要顯示的網頁中的各個部分。HTML文本是由HTML命令組成的描述性文本,HTML命令可以說明文字、圖片、音頻、視頻、超鏈接等,用戶在網頁上看到的各種元素都是通過HTML文本來實現的。
網頁的基本元素是通過HTML來實現的,但是HTML只能實現最基本的網頁樣式。隨著HTML的發展,為了滿足網頁開發者的需求,CSS便孕育而生。
CSS全稱為層疊樣式表。它為HTML語言提供了一種樣式描述,定義了元素的顯示方式。提供了豐富的樣式定義以及設置文本和背景屬性的能力。CSS可以將所有的樣式聲明統一存放,進行統一管理。在CSS中,一個文件的樣式可以從其他的樣式表中繼承。讀者在有些地方可以使用他自己更喜歡的樣式,在其他地方則繼承或“層疊”作者的樣式。這種層疊的方式使作者和讀者都可以靈活地加入自己的設計,混合每個人的愛好。
JavaScript(JS)是一種面向對象的解釋型腳本語言,它具有簡單、動態、跨平臺的特點。它被廣泛應用與Web開發中,幫助開發者構建可拓展的交互式Web應用。JavaScript由三部分組成:
網頁的基本結構大致可以分為四部分:Doctype聲明、html元素、head元素和body元素。
元素(Element)是網頁的一部分,是構成網頁的基本單位,實際上一個網頁就是由多個元素構成的的文本文件。 標簽(Tag)的作用就是用來定義元素。大多數的標簽都是成對使用的,它存在一個開始標簽與一個結尾標簽,開始與結尾標簽中間包含該元素的文本信息。
<div>這是一個div標簽</div>
<p>這是一個p標簽</p>
也有少部分的標簽不成對。
<input>
<img>
<hr>
...
屬性(attribute)主要是用來為標簽添加額外的信息,屬性的定義一般在開始標簽中,以鍵值對的形式出現(name="value" ),屬性的值應始終包括在引號內,屬性和屬性值對大小寫不敏感,但是推薦使用小寫的屬性與屬性值。一個標簽可以擁有多個屬性,也可以沒有屬性,開發者沒有為標簽定義屬性的話則會使用默認屬性。
<a href="https://www.kuaidaili.com/">這是一個a標簽,href是我的屬性。</a>
屬性在HTML中被分為兩種:通用屬性和專用屬性。 通用屬性適用于大部分或所有標簽之中,如:
專用屬性適用于小部分標簽或特定標簽,如:
DOM全稱即文檔對象模型,是W3C制定的標準接口規范,是一種處理HTML和XML文件的標準API。DOM將HTML文本作為一個樹形結構,DOM樹的每個結點都表示了一個HTML標簽或HTML標簽內的文本項,它將網頁與腳本或編程語言連接起來。
通過這個DOM樹,開發者可以通過JavaScript來創建動態HTML,開發者借助JavaScript可以實現:
DOM提供了一系列API來實現這些操作。
css選擇器是用來對HTML頁面中的元素進行控制,通過對CSS選擇器的了解,可以加深對網頁結構與節點的理解。常用的CSS選擇器主要分為:
1、元素選擇器: 通過標簽名{}的格式來選中對應標簽,如:p{}。
2、類選擇器: 通過.類名{}的格式來選中對應類名的標簽,如:.page{},page為元素的類名。
3、id選擇器: 通過#id值{}的格式來選中對應id值的標簽,如:#key{},key為元素的id值。
4、群組選擇器: 通過選擇器1,選擇器2,選擇器3...{}的格式來選中對應選擇器的標簽,如:div,.page{},即選擇div標簽下類名為pagae的標簽。
5、子元素選擇器: 通過父元素 > 子元素{}的格式來選中對應父元素中對應子元素的標簽,如:div > p{},即選擇div標簽下的p標簽,子元素選擇器只能選擇直接后代,不能跨節點選取。
6、后代選擇器: 通過父元素 子元素{}的格式來選中對應父元素中對應子元素的標簽,如:div p{},即選擇div標簽下的p標簽,后代選擇器可以跨節點選取。
談一個網頁打開的全過程(涉及DNS、CDN、Nginx負載均衡等)
從用戶在瀏覽器輸入域名開始,到web頁面加載完畢,這是一個說復雜不復雜,說簡單不簡單的過程,下文暫且把這個過程稱作網頁加載過程。下面我將依靠自己的經驗,總結一下整個過程。如有錯漏,歡迎指正。
閱讀本文需要讀者已有一定的計算機知識,了解TCP、DNS等。
眾所周知,打開一個網頁的過程中,瀏覽器會因頁面上的css/js/image等靜態資源會多次發起連接請求,所以我們暫且把這個網頁加載過程分成兩部分:
2.1 頁面加載
先上一張圖,直觀明了地讓大家了解下基本流程,然后我們再逐一分析。
2.1.1 DNS解析
什么是DNS解析?當用戶輸入一個網址并按下回車鍵的時候,瀏覽器得到了一個域名。而在實際通信過程中,我們需要的是一個IP地址。因此我們需要先把域名轉換成相應的IP地址,這個過程稱作DNS解析。
1) 瀏覽器首先搜索瀏覽器自身緩存的DNS記錄。
或許很多人不知道,瀏覽器自身也帶有一層DNS緩存。Chrome 緩存1000條DNS解析結果,緩存時間大概在一分鐘左右。
(Chrome瀏覽器通過輸入:chrome://net-internals/#dns 打開DNS緩存頁面)
2) 如果瀏覽器緩存中沒有找到需要的記錄或記錄已經過期,則搜索hosts文件和操作系統緩存。
在Windows操作系統中,可以通過 ipconfig /displaydns 命令查看本機當前的緩存。
通過hosts文件,你可以手動指定一個域名和其對應的IP解析結果,并且該結果一旦被使用,同樣會被緩存到操作系統緩存中。
Windows系統的hosts文件在%systemroot%\system32\drivers\etc下,linux系統的hosts文件在/etc/hosts下。
3) 如果在hosts文件和操作系統緩存中沒有找到需要的記錄或記錄已經過期,則向域名解析服務器發送解析請求。
其實第一臺被訪問的域名解析服務器就是我們平時在設置中填寫的DNS服務器一項,當操作系統緩存中也沒有命中的時候,系統會向DNS服務器正式發出解析請求。這里是真正意義上開始解析一個未知的域名。
一般一臺域名解析服務器會被地理位置臨近的大量用戶使用(特別是ISP的DNS),一般常見的網站域名解析都能在這里命中。
4) 如果域名解析服務器也沒有該域名的記錄,則開始遞歸+迭代解析。
這里我們舉個例子,如果我們要解析的是mail.google.com。
首先我們的域名解析服務器會向根域服務器(全球只有13臺)發出請求。顯然,僅憑13臺服務器不可能把全球所有IP都記錄下來。所以根域服務器記錄的是com域服務器的IP、cn域服務器的IP、org域服務器的IP……。如果我們要查找.com結尾的域名,那么我們可以到com域服務器去進一步解析。所以其實這部分的域名解析過程是一個樹形的搜索過程。
根域服務器告訴我們com域服務器的IP。
接著我們的域名解析服務器會向com域服務器發出請求。根域服務器并沒有mail.google.com的IP,但是卻有google.com域服務器的IP。
接著我們的域名解析服務器會向google.com域服務器發出請求。...
如此重復,直到獲得mail.google.com的IP地址。
為什么是遞歸:問題由一開始的本機要解析mail.google.com變成域名解析服務器要解析mail.google.com,這是遞歸。
為什么是迭代:問題由向根域服務器發出請求變成向com域服務器發出請求再變成向google.com域發出請求,這是迭代。
5) 獲取域名對應的IP后,一步步向上返回,直到返回給瀏覽器。
2.1.2 發起TCP請求
瀏覽器會選擇一個大于1024的本機端口向目標IP地址的80端口發起TCP連接請求。經過標準的TCP握手流程,建立TCP連接。
關于TCP協議的細節,這里就不再闡述。這里只是簡單地用一張圖說明一下TCP的握手過程。如果不了解TCP,可以選擇跳過此段,不影響本文其他部分的瀏覽。
2.1.3 發起HTTP請求
其本質是在建立起的TCP連接中,按照HTTP協議標準發送一個索要網頁的請求。
2.1.4 負載均衡
什么是負載均衡?當一臺服務器無法支持大量的用戶訪問時,將用戶分攤到兩個或多個服務器上的方法叫負載均衡。
什么是Nginx?Nginx是一款面向性能設計的HTTP服務器,相較于Apache、lighttpd具有占有內存少,穩定性高等優勢。
負載均衡的方法很多,Nginx負載均衡、LVS-NAT、LVS-DR等。這里,我們以簡單的Nginx負載均衡為例。關于負載均衡的多種方法詳情大家可以Google一下。
Nginx有4種類型的模塊:core、handlers、filters、load-balancers。
我們這里討論其中的2種,分別是負責負載均衡的模塊load-balancers和負責執行一系列過濾操作的filters模塊。
1) 一般,如果我們的平臺配備了負載均衡的話,前一步DNS解析獲得的IP地址應該是我們Nginx負載均衡服務器的IP地址。所以,我們的瀏覽器將我們的網頁請求發送到了Nginx負載均衡服務器上。
2) Nginx根據我們設定的分配算法和規則,選擇一臺后端的真實Web服務器,與之建立TCP連接、并轉發我們瀏覽器發出去的網頁請求。
Nginx默認支持 RR輪轉法 和 ip_hash法 這2種分配算法。
前者會從頭到尾一個個輪詢所有Web服務器,而后者則對源IP使用hash函數確定應該轉發到哪個Web服務器上,也能保證同一個IP的請求能發送到同一個Web服務器上實現會話粘連。
也有其他擴展分配算法,如:
fair:這種算法會選擇相應時間最短的Web服務器
url_hash:這種算法會使得相同的url發送到同一個Web服務器
3) Web服務器收到請求,產生響應,并將網頁發送給Nginx負載均衡服務器。
4) Nginx負載均衡服務器將網頁傳遞給filters鏈處理,之后發回給我們的瀏覽器。
而Filter的功能可以理解成先把前一步生成的結果處理一遍,再返回給瀏覽器。比如可以將前面沒有壓縮的網頁用gzip壓縮后再返回給瀏覽器。
2.1.5 瀏覽器渲染
1) 瀏覽器根據頁面內容,生成DOM Tree。根據CSS內容,生成CSS Rule Tree(規則樹)。調用JS執行引擎執行JS代碼。
2) 根據DOM Tree和CSS Rule Tree生成Render Tree(呈現樹)
3) 根據Render Tree渲染網頁
但是在瀏覽器解析頁面內容的時候,會發現頁面引用了其他未加載的image、css文件、js文件等靜態內容,因此開始了第二部分。
2.2 網頁靜態資源加載
以阿里巴巴的淘寶網首頁的logo為例,其url地址為 img.alicdn.com/tps/i2/TB1bNE7LFXXXXaOXFXXwFSA1XXX-292-116.png_145x145.jpg
我們清楚地看到了url中有cdn字樣。
什么是CDN?如果我在廣州訪問杭州的淘寶網,跨省的通信必然造成延遲。如果淘寶網能在廣東建立一個服務器,靜態資源我可以直接從就近的廣東服務器獲取,必然能提高整個網站的打開速度,這就是CDN。CDN叫內容分發網絡,是依靠部署在各地的邊緣服務器,使用戶就近獲取所需內容,降低網絡擁塞,提高用戶訪問響應速度。
接下來的流程就是瀏覽器根據url加載該url下的圖片內容。本質上是瀏覽器重新開始第一部分的流程,所以這里不再重復闡述。區別只是負責均衡服務器后端的服務器不再是應用服務器,而是提供靜態資源的服務器。
文章乃參考、轉載其他博客所得,僅供自己學習作筆記使用!!!
在本書的前幾章中,我們使用數據可視化來編碼數值數據。例如,第3章條形圖中條形的長度表示相關調查回復的數量。同樣,第4章折線圖中數據點的位置描繪了溫度。在本章中,我們將討論分層可視化,它對父子關系進行編碼,并且可以揭示迄今為止我們使用的更簡單的可視化所沒有注意到的模式。
分層可視化通過外殼、連接或鄰接來傳達父子關系。圖 11.1 顯示了使用外殼的分層可視化的兩個示例:圓形包和樹狀圖。顧名思義,圓包是一組圓圈。有一個根父級,最外圈,以及稱為節點的后續子級。節點的所有子節點都“打包”到該節點中,圓圈的大小與它們包含的節點數成正比。葉節點的大小(最低級別的子節點)可以表示任意屬性。樹狀圖的工作方式類似,但使用嵌套矩形而不是圓形。樹狀圖比圓形包更節省空間,我們經常在與財務相關的可視化中遇到它們。
可視化父子關系的一種熟悉且直觀的方法是通過連接,例如在樹形圖中。樹形圖可以是線性的,如家譜樹,也可以是徑向的,如圖 11.1 所示。線性樹形圖更易于閱讀,但會占用大量空間,而徑向樹更緊湊,但需要更多的努力來破譯。
最后,我們可以通過冰柱圖(也稱為分區層圖)的鄰接來可視化層次結構模式。我們經常在IT(信息技術)中遇到這樣的圖表。
圖 11.1 中顯示的圖表可能看起來多種多樣,但使用 D3 構建它們意味著涉及布局生成器函數的類似過程。在第 5 章中,我們了解了 D3 的布局生成器函數如何將信息添加到現有數據集,以及我們可以使用此信息將所需的形狀附加到 SVG 容器中。創建分層可視化也不例外。
在本章中,我們將構建兩個分層可視化:圓形包和線性樹形圖。我們將基于世界上 100 種使用最多的語言的數據集進行可視化。您可以看到我們將在 https://d3js-in-action-third-edition.github.io/100-most-spoken-languages/ 構建的圖表。
我們數據集中的每種語言都屬于一個語言家族或一組從共同祖先發展而來的相關語言。這些家族可以細分為稱為分支的較小組。讓我們以五種最常用的語言為例。在表 11.1 中,我們看到了如何將每種語言的信息存儲在電子表格中。左列包含語言:英語、中文普通話、印地語、西班牙語和法語。以下列包括相關的語系:印歐語系和漢藏語系,以及語言分支:日耳曼語系、漢尼特語系、印度-雅利安語系和羅曼語系。我們用每種語言的使用者總數和母語人士的數量來完成表格。
語言 | 家庭 | 分支 | 演講者總數 | 母語人士 |
英語 | 印歐語系 | 日耳曼 | 1,132m | 379m |
普通話 | 漢藏語 | 西尼特 | 1,117m | 918m |
印地語 | 印歐語系 | 印度-雅利安語 | 615m | 341m |
西班牙語 | 印歐語系 | 浪漫 | 534m | 460m |
法語 | 印歐語系 | 浪漫 | 280m | 77m |
分層可視化具有單個根節點,該節點分為多個以葉結尾的分支。在表 11.1 的示例數據集中,根節點可以稱為“語言”,如圖 11.2 所示。詞根分為兩個語系:印歐語系和漢藏語系,也分為分支:日耳曼語系、印度-雅利安語系、羅曼語系和漢尼語系。最后,葉子出現在圖形的右側:英語、印地語、西班牙語、法語和普通話。每種語言、分支、族和根稱為一個節點。
在本書的前半部分,我們主要使用類似遺產的項目結構。主要目標是進行簡單的設置,并專注于D3。但是,如果您發布 D3 項目,則很有可能使用 JavaScript 模塊導入。在本章中,我們將對項目結構進行現代化改造,以允許單獨導入 D3 模塊。它將使我們的項目文件更小,因此加載速度更快,并且將是查看哪些 D3 模塊包含哪種方法的絕佳機會。這些知識將使您將來更容易搜索 D3 文檔。
要將我們的 JavaScript 文件和 npm 模塊組合成一個瀏覽器可讀的模塊,我們需要一個捆綁器。您可能已經熟悉 Webpack 或 RollUp。由于此類工具可能需要相當多的配置,因此我們將轉向Parcel(https://parceljs.org/),這是一個非常易于使用且需要接近零的配置的捆綁器。
如果您的計算機上尚未安裝 Parcel,則可以使用以下命令對其進行全局安裝,其中 -g 代表全局。在終端窗口中運行此命令。
npm install -g parcel
我們建議使用此類全局安裝,因為它將使 Parcel 可用于您的所有項目。請注意,根據計算機的配置,您可能需要在命令的開頭添加 Mac 和 Linux 上的術語 sudo 或 Windows 上的 runas。
在您的計算機上安裝 Parcel 后,在代碼編輯器 (https://github.com/d3js-in-action-third-edition/code-files/tree/main/chapter_11/11.1-Formatting_hierarchical_data/start) 中打開本章代碼文件的起始文件夾。如果您使用的是 VS Code,請打開集成終端并運行命令 npm install 以安裝項目依賴項。在此階段,我們唯一的依賴項是允許我們稍后加載CSV數據文件。
若要啟動項目,請運行命令包,后跟根文件的路徑:
parcel src/index.html
在瀏覽器中打開 http://localhost:1234/ 以查看您的項目。每次保存文件時,瀏覽器中顯示的項目都會自動更新。完成工作會話后,您可以通過輸入終端 ctrl + C 來停止包裹。
在文件索引中.html ,我們已經加載了帶有腳本標簽的文件 main.js。因為我們將使用模塊,所以我們將腳本標記的 type 屬性設置為 module 。JavaScript 模塊的好處是,我們不需要將額外的腳本加載到 index 中.html ;一切都將從主.js.請注意,我們也不需要使用腳本標記加載 D3 庫。我們將從下一節開始安裝和導入所需的 D3 模塊。
為了創建分層可視化,D3 希望我們以特定方式格式化數據。我們有兩個主要選項:使用 CSV 文件或使用分層 JSON。
我們的大多數數據都以表格形式出現,通常以電子表格的形式出現。此類文件必須通過列指示父子關系。在表 11.2 中,我們將五種最常用的語言的示例數據集重新組織為名為“child”和“parent”的列。稍后我們將使用這些列名稱,讓 D3 知道如何建立父子關系。在第一行中,子列中有根節點“語言”。由于這是根節點,因此它沒有父節點。然后,在下面的行中,我們列出了根的直系子女:印歐語系和漢藏語系。他們都有“語言”作為父母。我們遵循語言分支(日耳曼語、漢尼特語、印度-雅利安語和羅曼語),并聲明哪個語系是它們的父語言。最后,每種語言(英語、中文普通話、印地語、西班牙語和法語)都有一行,并設置它們的父語言,即相關語言分支。我們還為每種語言設置了“total_speakers”和“native_speakers”列,因為我們可以在可視化中使用此信息,但這些信息對于分層布局不是必需的。
表 11.2 顯示了在使用 D3 構建分層可視化之前我們如何構建電子表格。然后,我們將其導出為CSV文件并將其添加到我們的項目中。請注意,您不必為本章的練習制作自己的電子表格。您可以在 /data 文件夾中找到 100 種最常用的語言(名為 flat_data.csv)格式正確的 CSV 文件。
孩子 | 父母 | 使用 | 母語 |
語言 | |||
印歐語系 | 語言 | ||
漢藏語 | 語言 | ||
日耳曼 | 印歐語系 | ||
西尼特 | 漢藏語 | ||
印度-雅利安語 | 印歐語系 | ||
浪漫 | 印歐語系 | ||
浪漫 | 印歐語系 | ||
英語 | 日耳曼 | 1,132m | 379m |
普通話 | 西尼特 | 1,117m | 918m |
印地語 | 印度-雅利安語 | 615m | 341m |
西班牙語 | 浪漫 | 534m | 460m |
法語 | 浪漫 | 280m | 77m |
讓我們flat_data.csv加載到我們的項目中!首先,在 /js 文件夾中創建一個新的 JavaScript 文件。將其命名為 load-data.js ,因為這是我們加載數據集的地方。在清單 11.1 中,我們創建了一個名為 loadCSVData() 的函數。我們向函數添加導出聲明,使其可供項目中的其他 JavaScript 模塊訪問。
要將 CSV 文件加載到我們的項目中,我們需要采用與使用 d3.csv() 方法不同的路線。宗地需要適當的轉換器才能加載 CSV 文件。我們已經通過安裝允許 Parcel 解析 CSV 文件的模塊為您完成了項目中的所有配置(有關更多詳細信息,請參閱文件 .parcelrc 和 .parcel-transformer-csv.json)。我們現在要做的就是使用 JavaScript require() 函數加載 CSV 文件并將其保存到常量 csvData 中。如果將 csvData 登錄到控制臺,您將看到它由一個對象數組組成,每個對象對應于 CSV 文件中的一行。我們遍歷 csvData 將說話者的數量格式化為數字并返回 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 中,我們使用導入語句來訪問函數 loadCSVData(),如清單 11.2 所示。然后我們將 loadCSVData() 返回的數組保存到一個名為 flatData 的常量中。
import { loadCSVData } from "./load-data.js"; #A
const flatData=loadCSVData(); #B
下一步是將平面 CSV 數據轉換為分層格式,或包含其子節點的根節點。d3-hierarchy 模塊 (https://github.com/d3/d3-hierarchy) 包含一個名為 d3.stratify() 的方法,它就是這樣做的。它還包括構建分層可視化所需的所有其他方法。
為了最大限度地提高項目性能,我們不會安裝整個 D3 庫,而只會安裝我們需要的模塊。讓我們從 d3 層次結構開始。在 VS Code 中,打開一個新的終端窗口并運行以下命令:
npm install d3-hierarchy
然后,創建一個名為 hierarchy 的新 JavaScript 文件.js 。在文件頂部,從 d3-hierarchy 導入 stratify() 方法,如清單 11.3 所示。然后,創建一個名為 CSVToHierarchy() 的函數,該函數將 CSV 數據作為參數。請注意,我們通過導出聲明提供此功能。
在 CSVToHierarchy() 中,我們通過調用方法 stratify() 來聲明一個層次結構生成器。在我們之前的設置中,我們會用 d3.stratify() 調用此方法。因為我們只安裝了 d3-hierarchy 模塊,所以我們不再需要在 d3 對象上調用方法并將 stratify() 視為一個獨立的函數。
要將我們的CSV數據轉換為分層結構,函數stratify()需要知道如何建立父子關系。使用 id() 訪問器函數,我們指示可以在哪個鍵下找到子項,在本例中為 子項(子項存儲在原始 CSV 文件的“子項”列中)。使用 parentId() 訪問器函數,我們指示可以在哪個鍵下找到父級,在我們的例子中是父級(父級存儲在原始 CSV 文件的“父級”列中)。
我們將數據傳遞給層次結構生成器,并將其保存在一個名為 root 的常量中,這是我們的分層數據結構。這種嵌套數據結構帶有一些方法,如 descendants(),它返回樹中所有節點的數組(“語言”、“印歐語”、“日耳曼語”、“英語”等),以及 leaves() 返回所有沒有子節點的數組(“英語”、“普通話”、“印地語”等)。我們將后代節點和葉節點保存到常量中,并使用根數據結構返回它們。
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 中,我們導入函數 CSVToHierarchy() 并調用它來獲取根、后代和葉。我們將在以下部分中使用此層次結構數據結構來生成可視化效果。
import { loadCSVData } from "./load-data.js";
import { CSVToHierarchy } from "./hierarchy.js";
const flatData=loadCSVData();
const [root, descendants, leaves]=CSVToHierarchy(flatData);
我們的數據集也可以存儲為分層 JSON 文件。JSON 本質上支持分層數據結構,并使其易于理解。以下 JSON 對象演示如何為示例數據集構建數據。在文件的根目錄中,我們有一個用大括號 ( {} ) 括起來的對象。根的“name”屬性是“語言”,其“子”屬性是一個對象數組。根的每個直接子級都是一個語言家族,其中包含語言分支的“子”數組,其中還包括帶有語言葉的“子”數組。請注意,每個子項都存儲在一個對象中。我們可以在葉對象中添加與語言相關的其他數據,例如說話者和母語人士的總數,但這是可選的。
{
"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 文件已在數據文件夾 ( hierarchical-data.json ) 中可用。我們將以類似的方式處理 CSV 文件以將其加載到我們的項目中。在清單 11.5 中,我們回到 load-data.js 并創建一個名為 loadJSONData() 的函數。此函數使用 JavaScript require() 方法來獲取數據集并將其存儲在名為 jsonData 的常量中。常量 jsonData 由函數返回。
export const loadJSONData=()=> {
const jsonData=require("../data/hierarchical-data.json");
return jsonData;
};
回到main.js,我們導入loadJSONData(),調用它并將它返回的對象存儲到一個名為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 文件生成分層數據結構,我們使用方法 d3.hierarchy() 。在示例 11.7 中,我們從 d3-hierarchy 導入層次結構函數。然后我們創建一個名為 JSONToHierarchy() 的函數,它將 JSON 數據作為參數。
我們調用 hierarchy() 函數并將數據作為參數傳遞。我們將它返回的嵌套數據結構存儲在名為 root 的常量中。與之前由 stratify() 函數返回的數據結構一樣,root 有一個方法后代 (),它返回樹中所有節點的數組(“語言”、“印歐語”、“日耳曼語”、“英語”等),還有一個方法 leaves() 返回所有沒有子節點的數組(“英語”、“普通話”、“印地語”等)。我們將后代節點和葉節點保存到常量中,并使用根數據結構返回它們。
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中,我們導入根,后代和葉子數據結構。為了將它們與從 CSV 數據導入的后綴區分開來,我們添加了_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);
在現實生活中的項目中,我們不需要同時加載CSV和JSON數據;這將是一個或另一個。我們這樣做只是出于教學目的。
有兩種主要方法可以將分層數據加載到 D3 項目中:從 CSV 文件或分層 JSON 文件。如圖 11.3 所示,如果我們使用 CSV 文件中的數據,我們會將其傳遞給 d3.stratify() 以生成層次結構數據結構。如果我們使用 JSON 文件,我們將使用 d3.hierarchy() 方法代替。這兩種方法都返回相同的嵌套數據結構,通常稱為 root 。此根有一個返回層次結構中所有節點的方法 descendants() 和一個返回沒有子節點的方法 leaves()。
現在我們的分層數據已經準備就緒,我們可以進入有趣的部分并構建可視化!這就是我們將在以下部分中執行的操作。
在圓形包中,我們用圓圈表示每個節點,子節點嵌套在其父節點中。當我們想要一目了然地理解整個分層組織時,這種可視化很有幫助。它易于理解,外觀令人愉悅。
在本節中,我們將使用圖 100.11 所示的圓形包可視化我們的 4 種最常用的語言數據集。在此可視化中,最外層的圓圈是根節點,我們將其命名為“語言”。顏色較深的圓圈是語系,顏色較淺的圓圈是語言分支。白色圓圈表示語言,其大小表示說話者的數量。
要使用 D3 創建圓形包裝可視化,我們需要使用布局生成器。與第5章中討論的類似,此類生成器將現有數據集附加構建圖表所需的信息,例如每個圓和節點的位置和半徑,如圖11.5所示。
我們已經有了名為 root 的分層數據結構,并準備跳到圖 11.5 中的第二步。首先,讓我們創建一個名為 circle-pack 的新 JavaScript 文件.js并聲明一個名為 drawCirclePack() 的函數。此函數采用根、后代,并將上一節中創建的數據結構保留為參數。
export const drawCirclePack=(root, descendants, leaves)=> {};
在main.js中,我們導入drawCirclePack()并調用它,將根,后代和葉作為參數傳遞。
import { drawCirclePack } from "./circle-pack.js";
drawCirclePack(root, descendants, leaves);
回到 circle-pack.js ,在函數 drawCirclePack() 中,我們將開始計算我們的布局。在示例 11.9 中,我們首先聲明圖表的維度。我們將寬度和高度都設置為 800px。然后,我們聲明一個邊距對象,其中上邊距、右邊距、下邊距和左邊距等于 1px。我們需要此邊距才能看到可視化的最外層圓圈。最后,我們使用第 4 章中采用的策略計算圖表的內部寬度和高度。
然后,我們調用 sum() 方法,該方法可用于 root。此方法負責計算可視化效果的聚合大小。我們還向 D3 指示應從中計算葉節點半徑的鍵:total_speakers 。
為了初始化包布局生成器,我們調用 D3 方法 pack() ,我們從文件頂部的 d3-hierarchy 導入該方法。我們使用它的 size() 訪問函數來設置圓形包的整體大小,并使用 padding() 函數將圓圈之間的空間設置為 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
};
如果將后代數組記錄到控制臺中,您將看到包布局生成器為每個節點追加了以下信息:
我們將在下一節中使用此信息來繪制圓形包。
我們現在準備繪制我們的圓形包!要選擇元素并將其附加到 DOM,我們需要通過在終端中運行 npm install d3-select 來安裝 d3 選擇模塊 (https://github.com/d3/d3-selection)。此模塊包含負責操作 DOM、應用數據綁定模式和偵聽事件的 D3 方法。在 circle-pack 的頂部.js ,我們從 d3-select 導入 select() 函數。
在 drawCirclePack() 中,我們將一個 SVG 容器附加到 div 中,其 id 為 “circle-pack”,該容器已存在于索引中.html 。我們按照第 4 章中解釋的策略設置其 viewBox 屬性并附加一個組以包含內部圖表。
然后,我們為后代數組中的每個節點附加一個圓圈。我們使用包布局生成器附加到數據的 x 和 y 值來設置它們的 cx 和 cy 屬性。我們對半徑做同樣的事情。現在,我們將圓圈的填充屬性設置為“透明”,將其筆觸設置為“黑色”。我們稍后會改變這一點。
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");
};
完成此步驟后,您的圓形包應如圖 11.6 所示。我們的可視化正在形成!
我們希望圓圈包中的每個語言家族都有自己的顏色。如果打開文件幫助程序.js ,您將看到一個名為 languageFamilies 的數組。它包含語言系列及其相關顏色的列表,如以下代碼片段所示。我們可以使用此數組來創建色階并使用它來設置每個圓的填充屬性。
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 。對于我們的色階,我們將使用序數刻度,它采用離散數組作為域、語言系列,將離散數組作為范圍,即關聯的顏色。在示例 11.11 中,我們創建了一個名為 scales.js 的新文件。在文件的頂部,我們從 d3-scale 導入 scaleOrdinal,從 helper.js 導入我們的 languageFamilies 數組。然后,我們聲明一個名為 colorScale 的序數刻度,傳遞一個語言家族標簽數組作為域,傳遞一個關聯顏色數組作為范圍。我們使用 JavaScript map() 方法生成這些數組。
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 節結束時,我們討論了 D3 包布局生成器如何將多條信息附加到后代數據集(也稱為節點),包括它們的深度。圖 11.7 顯示我們的圓形包的深度從 2 到 3 不等。表示“語言”根節點的最外層圓的深度為零。此圓圈具有灰色邊框和透明填充。以下圓圈是深度為一的語言家族。它們的 fill 屬性對應于我們剛剛聲明的色階返回的顏色。然后,語言分支的深度為 <>。他們繼承了父母顏色的更蒼白版本。最后,葉節點或語言的深度為 <>,顏色為白色。這種顏色漸變不遵循任何特定規則,但旨在使父子關系盡可能明確。
回到 circle-pack.js ,我們將使用色階設置圓圈的填充屬性。在文件的頂部,我們導入之前以比例創建的色階.js .為了生成語言分支的較淺顏色(深度為 2 的圓圈),我們將使用稱為插值的 d3 方法,該方法在 d3-插值模塊 (https://github.com/d3/d3-interpolate) 中可用。使用 npm 安裝 d3 插值安裝此模塊,并將此方法導入到圓包的頂部.js .
在示例 11.12 中,我們回到設置圓圈填充屬性的代碼。我們使用 JavaScript switch() 語句來評估附加到每個節點的深度數的值。如果深度為 3,則節點是一個語言家族。我們將它的 id 傳遞給色標,色標返回關聯的顏色。對于語言分支,我們仍然調用色階,但在其父節點的值上(d.parent.id)。然后,我們將比例返回的顏色作為 d0 插值() 函數的第一個參數傳遞。第二個參數是“白色”,即我們想要插入初始值的顏色。我們還將值 5.50 傳遞給 interpolate() 函數,以指示我們想要一個介于原始顏色和白色之間的 <>% 的值。最后,我們為所有剩余節點返回默認填充屬性“white”。
我們還更改了圓圈的筆觸屬性。如果深度為零,因此節點是最外層的圓,我們給它一個灰色的筆觸。否則,不應用筆畫。
...
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
};
完成后,您的彩色圓圈包應如圖 11.8 所示。
我們的圓圈包絕對看起來不錯,但沒有提供任何關于哪個圓圈代表哪種語言、分支或家族的線索。圓形包裝的主要缺點之一是在保持其可讀性的同時在其上貼標簽并不容易。但是由于我們正在從事數字項目,因此我們可以通過鼠標交互向讀者提供其他信息。
在本節中,我們將首先為較大的語言圈添加標簽。然后,我們將構建一個交互式工具,當鼠標位于葉節點上時,該工具可提供其他信息。
在我們的可視化中,我們處理的是名稱相對較短的語言,如法語或德語,以及其他名稱較長的語言,如“現代標準阿拉伯語”或“西旁遮普語”。要在相應的圓圈內顯示這些較長的標簽,我們需要讓它們分成多行。但是,如果您還記得我們之前關于 SVG 文本元素的討論,則可以在多行上顯示它們,但需要大量工作。使用常規 HTML 文本在需要時自動換行,這要容易得多!猜猜看:我們可以在 SVG 元素中使用常規 HTML 元素,這正是我們在這里要做的。
SVG 元素 foreignObject 允許我們在 SVG 容器中包含常規 HTML 元素,例如 div 。然后這個div可以像任何其他div一樣設置樣式,并且它的文本將在需要時自動換行。
在 D3 中,我們附加 foreignObject 元素的方式與其他任何元素相同。然后,在這些外來對象元素中,我們附加我們需要的 div。您可以將這些視為SVG和HTML世界之間的網關。
出于可讀性的目的,我們不會在每個語言圈上應用標簽,而只會在較大的語言圈上應用標簽。在示例 11.13 中,我們首先定義要應用標簽的圓的最小半徑,即 22px。然后,我們使用數據綁定模式為每個滿足最小半徑要求的葉節點附加一個 foreignObject 元素。外來對象元素有四個必需的屬性:
然后,我們需要指定要附加到 foreignObject 中的元素的 XML 命名空間。這就是為什么我們附加一個 xhtml:div 而不僅僅是一個 div .我們給這個div一個類名“leaf-label”,并將其文本設置為節點的id。文件可視化.css 已包含在 foreignObject 元素內水平和垂直居中標簽所需的樣式。
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
};
應用標簽后,您的圓形包應如圖 11.4 和托管項目 (https://d3js-in-action-third-edition.github.io/100-most-spoken-languages/) 中的包所示。現在,我們可以在可視化中找到主要語言。
在第 7 章中,我們討論了如何使用 D3 偵聽鼠標事件,例如顯示工具提示。在本節中,我們將構建類似的東西,但不是在可視化效果上顯示工具提示,而是將其移動到一側。只要您有超過幾行信息要向用戶顯示,這是一個很好的選擇。由于此類工具提示是使用 HTML 元素構建的,因此也更容易設置樣式。
在文件索引中.html ,取消注釋 id 為“信息容器”的 div。此 div 包含兩個主要元素:
在示例 11.14 中,我們又回到了 circle-pack.js 。在文件頂部,我們從 d3-select 導入 selectAll 函數。我們還需要安裝 d3 格式模塊 (https://github.com/d3/d3-format) 并導入其格式函數。
為了區分節點級別,在清單 11.14 中,我們將它們的深度值添加到它們的類名中。然后,我們使用 selectAll() 函數選擇所有類名為 “pack-circle-depth-3” 的圓圈和所有 foreignObject 元素。我們使用 D3 on() 方法將 mouseenter 事件偵聽器附加到葉節點及其標簽。在此事件偵聽器的回調函數中,我們使用附加到元素的數據來填充有關相應語言、分支、家族和說話人數量的工具提示信息。請注意,我們使用 format() 函數來顯示具有三個有效數字和后綴的揚聲器數量,例如“M”表示“百萬”(“.3s”);
然后,我們通過添加和刪除類名“hidden”來隱藏說明并顯示工具提示。我們還在鼠標離開語言節點或其標簽時應用事件偵聽器。在其回調函數中,我們隱藏工具提示并顯示說明。
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
});
};
當您將鼠標移到語言節點上時,您現在應該會看到有關分支、系列和說話人數量的其他信息顯示在可視化效果的右側,如圖 11.9 所示。
圓形包的一個缺點是它們很難在移動屏幕上呈現。盡管圓圈仍然在小屏幕上提供了父子關系的良好概述,但標簽變得更加難以閱讀。此外,由于語言圈可能會變小,因此使用觸摸事件顯示信息可能會很棘手。為了解決這些缺點,我們可以將語言家族相互堆疊,或者在移動設備上選擇不同類型的可視化。
可視化父子關系的一種熟悉且直觀的方法是使用樹形圖。樹形圖類似于家譜樹。像圓形包一樣,它們由節點組成,但也顯示了它們之間的鏈接。在本節中,我們將構建 100 種最常用的語言的樹形圖,如圖 11.10 所示。左側是根節點,即“語言”。它分為語系,也細分為語言分支,最后是語言。我們用圓圈的大小可視化每種語言的使用者總數。至于圓圈包,這些圓圈的顏色代表它們所屬的語言家族。
與上一節中構建的圓形包類似,D3樹形圖是使用布局生成器d3.tree()創建的,它是d3層次結構模塊(https://github.com/d3/d3-hierarchy)的一部分。然后,我們使用布局提供的信息來繪制鏈接和節點。
要生成樹布局,讓我們首先創建一個新文件并將其命名為 tree.js .在這個文件中,我們創建了一個名為drawTree()的函數,它將分層數據(也稱為根,后代和葉)作為參數。在示例 11.15 中,我們聲明了圖表的維度。我們給它一個 1200px 的寬度,圖表的 HTML 容器的寬度,以及 3000px 的高度。請注意,高度與圖表中的葉節點數成正比,并且是通過反復試驗找到的。處理樹可視化時,請從大致值開始,并在可視化顯示在屏幕上后進行調整。
為了生成布局,我們調用 D3 的 tree() 函數,我們從文件頂部的 d3-hierarchy 導入該函數,并設置其 size() 訪問器函數,該函數將圖表的寬度和高度數組作為參數。因為我們希望我們的樹從左到右展開,所以我們首先傳遞 innerHeight,然后是 innerWidth 。如果我們希望樹從上到下部署,我們會做相反的事情。最后,我們將分層數據(根)傳遞給樹布局生成器。
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中,我們還需要導入drawTree()函數并將根、后代和葉作為參數傳遞。
import { drawTree } from "./tree.js";
drawTree(root, descendants, leaves);
生成布局后,繪制樹形圖非常簡單。像往常一樣,我們首先需要附加一個 SVG 容器并設置其 viewBox 屬性。在示例 11.16 中,我們將這個容器附加到 div 中,其 id 為 “tree”,該 id 已存在于 index.html 中。請注意,我們必須從文件頂部的 d3-select 模塊導入 select() 函數。我們還將一個 SVG 組附加到此容器,并根據前面定義的左邊距和上邊距進行轉換,遵循自第 4 章以來使用的策略。
要創建鏈接,我們需要 d3.link() 鏈接生成器函數。此函數的工作方式與第 3 章中介紹的線路生成器完全相同。它是 d3 形狀模塊 (https://github.com/d3/d3-shape) 的一部分,我們使用命令安裝它 npm 安裝 d3 形狀 .在文件的頂部,我們從 d3-shape 導入 link() 函數,以及 curveBumpX() 函數,我們將使用它來確定鏈接的形狀。
然后我們聲明一個名為 linkGenerator 的鏈接生成器,它將曲線函數 curveBumpX 傳遞給 D3 的 link() 函數。我們將它的 x() 和 y() 訪問器函數設置為使用樹布局生成器存儲在 y 和 x 鍵中的值。就像我們準備樹布局生成器時一樣,x 和 y 值是反轉的,因為我們希望從右到左而不是從上到下繪制樹。
為了繪制鏈接,我們使用數據綁定模式從 root.links() 提供的數據中附加路徑元素。此方法返回樹的鏈接數組及其源點和目標點。然后調用鏈接生成器來計算每個鏈接或路徑的 d 屬性。最后,我們設置鏈接的樣式并將其不透明度設置為 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
};
準備就緒后,您的鏈接將類似于圖 11.12 中的鏈接。請注意,此圖僅顯示部分視圖,因為我們的樹非常高!
為了突出顯示每個節點的位置,我們將在樹形圖中附加圓圈。帶有灰色筆劃的小圓圈將表示根、語言家族和語言分支節點。相反,語言節點圓圈的大小與說話者總數成比例,并且具有與其語言家族關聯的顏色。
要計算語言節點的大小,我們需要一個規模。在清單 11.17 中,我們轉到 scales.js并從 d3-scale 導入 scaleRadial()。量表的域是連續的,從零擴展到數據集中說其中一種語言的最大人數。它的范圍可以在 83 到 <>px 之間變化,這是上一節中創建的圓包中最大圓的半徑。
因為最大說話人數只有在我們檢索數據并創建層次結構(根)后才可用,我們需要將徑向刻度包裝到一個名為 getRadius() 的函數中。當我們需要計算圓的半徑時,我們將傳遞當前語言的說話者數量以及最大說話者數量,此函數將返回半徑。
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() 計算最大揚聲器數。要使用這種方法,我們需要安裝 d3-array 模塊 (https://github.com/d3/d3-array) 與 npm install d3-array ,并在文件頂部導入 max() 函數。我們還從 scales 導入函數 getRadius() 和色階.js .
然后,我們使用數據綁定模式將一個圓附加到每個后代節點的內部圖表中。我們使用樹布局生成器附加到數據中的 x 和 y 鍵來設置這些圓圈的 cx 和 cy 屬性。如果圓是一個葉節點,我們根據相關語言的說話者數量和 getRadius() 函數設置其半徑。我們使用色階設置其顏色,填充不透明度為 30%,描邊設置為“無”。其他圓圈的半徑為 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
};
為了完成樹形圖,我們為每個節點添加一個標簽。在示例 11.19 中,我們使用數據綁定模式為數據集中的每個節點附加一個文本元素。如果標簽與葉節點相關聯,則在右側顯示標簽。否則,標簽將位于其節點的左側。我們還為標簽提供白色筆觸,以便它們在放置在鏈接上時更易于閱讀。通過將繪制順序屬性設置為“描邊”,我們可以確保在文本填充顏色之前繪制描邊。這也有助于提高可讀性。
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);
};
完成后,您的樹形圖應類似于托管項目 (https://d3js-in-action-third-edition.github.io/100-most-spoken-languages/) 和圖 11.13 中的樹形圖。
此樹形圖的線性布局使其相對容易地轉換為移動屏幕,只要我們增加標簽的字體大小并確保它們之間有足夠的垂直空間。有關構建響應式圖表的提示,請參閱第 9 章。
為了完成這個項目,我們需要為語言家族的顏色和圓圈的大小添加一個圖例。我們已經為您構建了它。要顯示圖例,請轉到索引.html然后取消注釋帶有“圖例”類的 div。然后,在main.js中,從legend導入函數createLegend(.js并調用它來生成legend。請參閱第7章中的鯨目動物可視化,以獲取有關我們如何構建這個傳說的更多解釋。看看圖例中的代碼.js甚至更好的是,嘗試自己構建它!
在本章中,我們討論了如何構建圓形包和樹形圖可視化。使用 D3 制作其他層次結構表示形式(如樹狀圖和冰柱圖)非常相似。
圖 11.14 說明了我們如何從 CSV 或 JSON 數據開始,我們將其格式化為名為 root 的分層數據結構。使用這種數據結構,我們可以構建任何分層可視化,唯一的區別是用作布局生成器的功能。對于圓形包,布局生成器函數為 d3.pack() ;對于樹狀圖,d3.treemap() ;對于樹形圖,d3.tree() ;對于冰柱圖,d3.partition() 。我們可以在 d3 層次結構模塊 (https://github.com/d3/d3-hierarchy) 中找到這些布局生成器。
現在,您已經掌握了構建 100 種最常用的語言的樹狀圖所需的所有知識,如下圖所示,以及托管項目 (https://d3js-in-action-third-edition.github.io/100-most-spoken-languages/)。樹狀圖將分層數據可視化為一組嵌套矩形。傳統上,樹狀圖僅顯示葉節點,在我們的例子中,葉節點是語言。矩形或葉節點的大小與每種語言的使用者總數成正比。
1. 在索引中.html添加一個 id 為“樹狀圖”的 div。
2. 使用一個名為 drawTreemap() 的函數創建一個名為 treemap.js 的新文件。此函數接收根數據結構和葉作為參數,并從 main.js 調用。
3. 使用 d3.treemap() 布局生成器計算樹狀圖布局。使用 size() 訪問器函數,設置圖表的寬度和高度。您還可以使用填充Inner()和paddingOuter()指定矩形之間的填充。有關更深入的文檔 (https://github.com/d3/d3-hierarchy),請參閱 d3 層次結構模塊。
4. 將 SVG 容器附加到 div,ID 為“樹狀圖”。
5. 為每個葉節點附加一個矩形。使用布局生成器添加到數據集的信息設置其位置和大小。
6. 在相應的矩形上附加每種語言的標簽。您可能還希望隱藏顯示在較小矩形上的標簽。
如果您在任何時候遇到困難或想將您的解決方案與我們的解決方案進行比較,您可以在附錄 D 的 D.11 節和文件夾 11.4-樹狀圖/本章代碼文件的末尾找到它。但是,像往常一樣,我們鼓勵您嘗試自己完成它。您的解決方案可能與我們的略有不同,沒關系!
*請認真填寫需求信息,我們會在24小時內與您取得聯系。