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 日韩中文字幕精品免费一区,国产综合91,韩剧2023年最新电视剧在线观看

          整合營銷服務(wù)商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          D3.js實戰(zhàn)教程:14 創(chuàng)建自定義可視化

          D3.js實戰(zhàn)教程:14 創(chuàng)建自定義可視化

          章涵蓋

          • 探索定量和定性數(shù)據(jù)的可視化渠道。
          • 將復(fù)雜的項目分解為小的 UI 組件。
          • 組合不同的 D3 方法以創(chuàng)建自定義可視化效果。
          • 創(chuàng)建響應(yīng)式 SVG 網(wǎng)格。
          • 在徑向布局上定位視覺元素。

          在本書的前三部分中,我們一直在應(yīng)用各種 D3 技術(shù)來開發(fā)眾所周知的可視化布局,如條形圖、流圖、直方圖、地圖等。但是,如果您選擇 D3 作為數(shù)據(jù)可視化工具,那么您很有可能還希望構(gòu)建復(fù)雜且不尋常的可視化。若要創(chuàng)建獨特的項目,需要了解 D3 可以使用的不同方法和布局。與其說是詳細(xì)了解每種方法,不如說是掌握 D3 背后的哲學(xué),并知道在需要時在哪里查找信息。附錄 C 中,我們映射了所有 D3 模塊及其內(nèi)容,可以為您提供幫助。創(chuàng)建自定義布局所需的另一項技能是將想法和幾何分解為代碼的能力,我們將在本章的項目中執(zhí)行此操作。

          該項目將帶您了解創(chuàng)建完全自定義可視化的幕后情況,從草圖創(chuàng)意到將項目分解為組件,再到將視覺元素渲染到徑向布局上。我們將建造的項目探索了文森特梵高在他生命的最后十年中產(chǎn)生的藝術(shù)遺產(chǎn)。您可以在 https://d3js-in-action-third-edition.github.io/van_gogh_work/ 找到已完成的項目。

          我們將遵循一個六步過程來使這個項目栩栩如生。雖然這不是一成不變的,但這大致是任何數(shù)據(jù)可視化項目都可以遵循的方法。

          1. 收集數(shù)據(jù)。
          2. 探索數(shù)據(jù)。
          3. 草繪布局。
          4. 構(gòu)建項目框架。
          5. 創(chuàng)建可視化效果。
          6. 規(guī)劃有意義的交互。

          14.1 收集數(shù)據(jù)

          收集和清理數(shù)據(jù)是任何數(shù)據(jù)可視化項目中最關(guān)鍵的一步。如果幸運的話,我們得到了現(xiàn)成的數(shù)據(jù)集,可以直接開始可視化,就像本書以前的項目一樣。但通常情況下,我們需要從不同來源收集數(shù)據(jù),對其進(jìn)行分析,清理數(shù)據(jù)并對其進(jìn)行格式化。數(shù)據(jù)收集和操作可能需要大量的時間。它需要耐心和勤奮。在本節(jié)中,我們將討論為本章項目準(zhǔn)備數(shù)據(jù)所經(jīng)歷的不同步驟。

          但在我們尋找數(shù)據(jù)之前,讓我們花點時間定義我們想要可視化的信息類型。這個項目的靈感來自Frederica Fragapane的數(shù)據(jù)可視化研討會,在此期間,我們使用文森特梵高寫給他兄弟西奧的信的數(shù)據(jù)集。我們對梵高的豐富文學(xué)遺產(chǎn)感到震驚,并認(rèn)為將其與他的著名繪畫和素描相結(jié)合以深入了解他的整個藝術(shù)遺產(chǎn)會很有趣。

          所以,我們知道我們想收集有關(guān)梵高的繪畫、素描和信件的數(shù)據(jù)。理想情況下,我們希望及時放置這些作品,以可視化他藝術(shù)作品的起伏。經(jīng)過幾次谷歌搜索,我們找到了以下資源:

          • 文森特梵高的作品列表:維基百科頁面列出了梵高創(chuàng)作的每幅畫,以及他隨信件一起寄出的草圖,按媒介和時期分組。
          • 文森特梵高的繪畫列表:維基百科頁面列出了梵高創(chuàng)作的每幅畫,除了他的信件草圖。
          • vangoghletters.org:一個專門介紹梵高信件的網(wǎng)站,按時期分組。
          • 文森特·梵高:一個關(guān)于梵高生平的維基百科頁面,可以幫助我們更好地了解塑造他藝術(shù)作品的事件。

          通過探索這些資源,我們還注意到,我們可以根據(jù)梵高居住的城市將他的生活分解為幾個階段。例如,他于1886年從荷蘭搬到巴黎,在那里他遇到了保羅·高更和亨利·德·圖盧茲-勞特累克,僅舉兩例。這些藝術(shù)相遇無疑影響了梵高的作品。我們還知道,他從 1889 年 1890 月到 1890 年 <> 月在圣保羅德莫索萊精神病院住院。在此期間,他開始將漩渦融入他對醫(yī)院花園的描繪中。最后,梵高于 <> 年 <> 月自殺身亡,標(biāo)志著他多產(chǎn)的十年藝術(shù)創(chuàng)作的戛然而止。意識到這些事件,我們希望我們的可視化構(gòu)成梵高過去十年的時間線。

          現(xiàn)在,我們需要從找到的資源中提取數(shù)據(jù)。讓我們以繪畫為例(https://en.wikipedia.org/wiki/List_of_works_by_Vincent_van_Gogh)。這個維基百科頁面包含一系列表格,列出了一千多幅畫作。不是我們想要手動提取的東西!您可以找到從網(wǎng)頁中提取表并將其轉(zhuǎn)換為 CSV 文件(如 tableconvert.com)的聯(lián)機服務(wù)。此類工具使用方便快捷。但是如果我們想要更細(xì)粒度的控制,我們可以編寫一個簡單的腳本來完成這項工作。

          14.1 例包含一個腳本,您可以使用該腳本從維基百科頁面中提取每幅畫的標(biāo)題、圖像 URL 和媒介。要使用此腳本,請打開瀏覽器的控制臺,復(fù)制粘貼整個代碼段,然后單擊 Enter 。

          如果我們看一下頁面結(jié)構(gòu),我們會發(fā)現(xiàn)它由一系列HTML表格組成,每個表格都包含使用相同媒介制作的繪畫列表。前六張表是關(guān)于油畫的;第七幅包含水彩畫;第八和第九是關(guān)于石版畫和蝕刻版畫的,我們將將它們歸入“印刷”媒介。最后一個表格包含字母草圖,我們還不想提取。在示例 14.1 中,我們聲明了一個數(shù)組,其中包含我們感興趣的表的索引及其相關(guān)介質(zhì)。

          然后,我們使用文檔方法querySelectorAll()和類“wikitable”和“sortable”作為選擇器從頁面中提取所有HTML表。我們通過打開瀏覽器檢查器并仔細(xì)查看標(biāo)記來找到這個選擇器,以找到我們感興趣的表的唯一且通用的選擇器。

          在循環(huán)遍歷這些表時,我們檢查它們是否已存在于腳本開頭聲明的 tables 數(shù)組中。這種驗證使我們能夠避免從字母草圖表中提取信息。然后,我們可以遍歷每個表格行并提取繪畫圖像的標(biāo)題和 URL。請注意我們必須如何在代碼中適應(yīng)不同的 DOM 結(jié)構(gòu),因為表行的格式不一致。與這些HTML結(jié)構(gòu)不匹配的繪畫將被賦予標(biāo)題和圖像URL為null,稍后將手動完成。處理現(xiàn)實生活中的數(shù)據(jù)通常是混亂的!您還將看到我們從 srcset 屬性而不是 src 中提取圖像 URL,因為此圖像更小,并且在我們的項目中需要更少的加載時間。

          最后,我們將繪畫信息構(gòu)建成一個對象,并將其推送到一個名為“繪畫”的數(shù)組中。但是,通過將此數(shù)組記錄到控制臺中,我們可以將其復(fù)制粘貼到代碼編輯器中并創(chuàng)建一個 JSON 文件。

          此腳本針對此特定示例量身定制,在其他網(wǎng)頁上沒有幫助。但是你可以看到你的JavaScript技能對于從網(wǎng)頁中提取任何信息是多么有價值。

          事例 14.1 從梵高的畫作中提取繪畫信息的腳本 維基百科頁面

          const tables=[                       #A
            { index: 0, medium: "oil" },         #A
            { index: 1, medium: "oil" },         #A
            { index: 2, medium: "oil" },         #A
            { index: 3, medium: "oil" },         #A
            { index: 4, medium: "oil" },         #A
            { index: 5, medium: "oil" },         #A
            { index: 6, medium: "watercolor" },  #A
            { index: 7, medium: "print" },       #A
            { index: 8, medium: "print" },       #A
          ];                                     #A
           
          const domTables=document.querySelectorAll(".wikitable.sortable");  #B
          const paintings=[];  #C
           
          domTables.forEach((table, i)=> {                     #D
            if (i <=tables.length - 1) {                       #D
              const medium=tables[i].medium;                  #D
                                                                #D
              const rows=table.querySelectorAll("tbody tr");  #D
              rows.forEach(row=> {                             #D
                let title;                                                     #E
                if (row.querySelector(".thumbcaption i a")) {                  #E
                  title=row.querySelector(".thumbcaption i a").textContent;  #E
                } else if (row.querySelector(".thumbcaption i")) {             #E
                  title=row.querySelector(".thumbcaption i").textContent;    #E
                } else {                                                       #E
                  title=null;                                                #E
                }                                                              #E
           
                let imageLink;                                                     #F
                if (row.querySelector(".thumbinner img")) {                        #F
                  const image=row.querySelector(".thumbinner img").srcset;       #F
                  imageLink=`https${image.slice(image.indexOf("1.5x, ")+6,-3)}`; #F
                } else {                                                           #F
                  imageLink=null;                                                #F
                }                                                                  #F
           
                paintings.push({         #G
                  title: title,          #G
                  imageLink: imageLink,  #G
                  medium: medium         #G
                });                      #G
              })
            }
          });
           
           
          console.log(paintings);  #H

          清單 14.1 中的腳本示例不完整。我們?nèi)匀恍枰崛∶糠嫷娜掌凇⒊叽纭?dāng)前位置和創(chuàng)作位置。為了避免本節(jié)太長,我們不會在這里這樣做,但如果您想練習(xí)從網(wǎng)頁中提取數(shù)據(jù),請嘗試一下!請注意,我們還必須操作提取的數(shù)據(jù)以分別存儲繪畫的寬度和高度,以及創(chuàng)作的月份和年份。需要一些額外的研究來找到一些繪畫的創(chuàng)作月份并找到它們的主題(肖像、靜物、風(fēng)景等)。

          如果您想直接跳轉(zhuǎn)到使用數(shù)據(jù),本章的代碼文件包含梵高的繪畫、素描、信件和他所居住城市的時間軸的現(xiàn)成數(shù)據(jù)集(見 https://github.com/d3js-in-action-third-edition/code-files/tree/main/chapter_14/14.4.1-Responsive_SVG_container/start/src/data)。

          14.2 探索數(shù)據(jù)

          在第3章中,我們定義了兩類主要數(shù)據(jù):定量定性,如圖14.1所示。定量數(shù)據(jù)由數(shù)字信息組成,例如股票市場行為價值的起伏或教室里的學(xué)生人數(shù)。定量數(shù)據(jù)可以是離散的,由無法細(xì)分的整數(shù)組成,也可以是連續(xù)的,其中數(shù)字在細(xì)分為較小的單位時仍然有意義。另一方面,定性數(shù)據(jù)是非數(shù)字信息,例如國家列表或星巴克咖啡訂單的可用尺寸(矮、高、大、通風(fēng)等)。定性數(shù)據(jù)可以是名義數(shù)據(jù)(值沒有特定順序)或順序(順序很重要)。

          圖14.1 我們可以將數(shù)據(jù)分為兩大類:定量(數(shù)字信息)和定性(非數(shù)字信息)。定量數(shù)據(jù)是離散的或連續(xù)的,而定性數(shù)據(jù)是名義的或有序的。


          由于我們不會使用相同的通道來可視化不同的數(shù)據(jù)類型,因此編寫一個可用于項目的變量列表并按數(shù)據(jù)類型組織它們通常很有幫助。此步驟可以幫助我們識別可以使用的不同視覺通道或編碼數(shù)據(jù)的方法。圖14.2說明了定量數(shù)據(jù)通常通過位置(如散點圖)、長度(如條形圖)、面積(如我們的羅密歐與朱麗葉項目中節(jié)點的大小)(見第12章)、角度(如餅圖)或連續(xù)色標(biāo)進(jìn)行可視化。另一方面,定性數(shù)據(jù)通常使用分類色階圖案符號連接(如網(wǎng)絡(luò)圖)或分層數(shù)據(jù)的外殼(如圓形包)進(jìn)行翻譯。這樣的列表只能是不完整的,因為只要有一點創(chuàng)造力,我們就可以設(shè)計出可視化數(shù)據(jù)的新方法。但它提供了我們可以使用的主要視覺編碼的概述。

          圖 14.2 定量數(shù)據(jù)通常使用位置、長度、面積、角度和連續(xù)色標(biāo)可視化,而定性數(shù)據(jù)使用分類色標(biāo)、圖案、符號、連接和外殼。


          在這一點上,一個有用的練習(xí)包括列出數(shù)據(jù)集中包含的不同數(shù)據(jù)屬性,識別定量和定性數(shù)據(jù),并集思廣益我們希望如何可視化主要屬性。在圖 14.3 中,我們列出了該項目的四個數(shù)據(jù)集(梵高的畫作列表、他的繪畫列表、他每月寫的信數(shù)量以及他職業(yè)生涯中居住的城市的時間線),并確定數(shù)據(jù)屬性是定量的(藍(lán)點)還是定性的(紅點)。基于這些信息,我們可以開始考慮要創(chuàng)建的可視化。

          圖 14.3 在數(shù)據(jù)可視化開始時,列出我們必須識別為定量(藍(lán)點)和定性數(shù)據(jù)(紅點)的所有數(shù)據(jù)屬性會很有幫助。然后,我們可以開始集思廣益,討論如何可視化這些數(shù)據(jù)屬性。


          在這個項目中,我們希望在時間軸上展示梵高的藝術(shù)作品(繪畫、素描和信件),以探索每種表達(dá)方式的使用與藝術(shù)家在荷蘭和法國的移動如何演變之間的相關(guān)性。我們希望更多地關(guān)注繪畫,并允許用戶單獨探索它們。如果圓圈代表每幅畫,我們可以使用圓圈的顏色來傳達(dá)繪畫的主題(肖像、靜物、風(fēng)景等),使用它們的大小作為作品的尺寸,并用圓圈的邊框突出顯示介質(zhì)(油畫、水彩或印刷品),如圖 14.4 所示。這些圓圈將定位在某種時間軸上。

          每月制作的圖紙和字母數(shù)量可以通過條形圖或面積圖的長度作為次要信息添加。最后,我們知道我們需要一些可點擊的時間線來選擇和突出梵高在他生命不同時期的作品。

          圖 14.4 在探索了數(shù)據(jù)屬性并確定了它們的類型之后,我們決定用一個位于時間軸上的圓圈來表示每幅畫。圓圈的顏色、大小和邊框?qū)鬟_(dá)繪畫的主題、尺寸和媒介。我們將通過條形圖的長度和面積圖可視化繪圖和字母的數(shù)量,面積圖也位于時間軸上。


          14.3 繪制布局草圖

          一旦選擇了視覺通道,我們就可以開始繪制項目的布局。我們已經(jīng)確定每幅畫將由一個圓圈表示并定位在時間軸上。水平軸或垂直軸可以工作,盡管它對于屏幕來說可能太大。一個有趣的解決方法可能是徑向時間軸。與其有一個很難適應(yīng)移動屏幕的大圓圈,不如使用小倍數(shù)方法。小型序列圖是一系列可視化效果,使用相同的比例和軸,但表示數(shù)據(jù)的不同方面。通過這種方法,我們可以每年有一個輪子,允許我們將它們定位到一個網(wǎng)格中,如圖 14.5 所示。在桌面上,我們將在左側(cè)顯示可點擊的時間線,在右側(cè)以三列網(wǎng)格形式布置小型序列可視化。在平板電腦上,網(wǎng)格將減少到兩列,而在移動設(shè)備上,我們將使用沒有時間軸功能的單列網(wǎng)格。

          圖 14.5 小型多個可視化是共享相同比例和軸的一系列圖表。它們通常布置成網(wǎng)格,使其易于適應(yīng)不同的屏幕尺寸。


          每個小倍數(shù)將可視化一整年,月份沿圓周分布。對于每個月,圖紙的數(shù)量將由面積圖和條形長度的字母數(shù)量表示。代表一個月內(nèi)繪畫的圓圈將聚集在一起,如圖 14.6 所示。

          圖 14.6 月份將在每個小倍數(shù)中以圓形模式排列。每個月圖紙的數(shù)量將通過面積圖顯示,而字母的數(shù)量將由條形的長度表示。一個月內(nèi)創(chuàng)作的畫作將分組在一起。


          下一步是創(chuàng)建調(diào)色板并選擇字體。我們需要為八種不同的繪畫主題提供一個分類的調(diào)色板:自畫像、肖像、農(nóng)民生活、室內(nèi)場景、靜物、風(fēng)景、城市景觀建筑等,以及字母和素描的另一種顏色。創(chuàng)建任何調(diào)色板時,請考慮要在項目中安裝的氛圍。例如,在這里,我們想使用一種歡快的調(diào)色板,靈感來自梵高生命中最后幾年的畫作中的色調(diào)。我們通過從繪畫中提取金色并使用 coolors.co 生成匹配的顏色,從圖 14.7 創(chuàng)建了分類調(diào)色板。對于分類調(diào)色板來說,八種顏色已經(jīng)很多了,因此我們不得不對某些類別使用類似的色調(diào)。例如,我們?yōu)樾は瘢?c16e70)選擇了舊玫瑰色,為自畫像選擇了相同顏色的較亮版本(#f7a3a6)。您還可以在 adobe.color.com 和 colorhunt.co 上找到調(diào)色板的靈感。

          圖 14.7 對于這個項目,我們需要為繪畫主題使用八種顏色的分類調(diào)色板,并為字母和圖畫提供額外的顏色。通過調(diào)整亮度,我們在文本和背景的另外兩種變體中拒絕了最后一種顏色。


          對于字體,我們發(fā)現(xiàn) font.google.com 是免費網(wǎng)絡(luò)字體的絕佳資源。通常,您希望每個項目最多堅持兩個字體系列,一個用于標(biāo)題,一個用于文本正文。一個簡單的谷歌搜索將為谷歌字體組合提供很多想法。對于這個項目,我們選擇了“Libre Baskerville Bold”作為標(biāo)題,一種與19世紀(jì)相呼應(yīng)的襯線字體,文本和標(biāo)簽為“Source Sans Pro”,一種無襯線字體,對用戶界面具有出色的可讀性。

          14.4 構(gòu)建項目骨架

          一旦我們知道我們想要構(gòu)建什么,我們必須決定我們要使用的基礎(chǔ)設(shè)施。因為這個項目比我們在本書前面創(chuàng)建的項目更復(fù)雜,所以我們將轉(zhuǎn)向JavaScript框架。使用框架將使我們能夠?qū)㈨椖糠纸鉃樾〗M件,使其更易于開發(fā)和維護(hù)。我們已經(jīng)在第 8 章中使用 React 構(gòu)建了一個項目,所以這一次,我們將選擇 Svelte,一個數(shù)據(jù)可視化社區(qū)特別喜歡的簡單編譯器。如果您還不熟悉Svelte,請不要擔(dān)心。本章的重點仍將放在創(chuàng)建復(fù)雜數(shù)據(jù)可視化項目背后的一般原則上。您可以一起閱讀并收集一點點智慧,而不必潛入 Svelte。如果您以前玩過 Svelte 或想嘗試一下,您會發(fā)現(xiàn)它非常直觀,并且可以很好地與 D3 配合使用。您可以在附錄 E 中找到對 Svelte 的簡要介紹,并在 https://svelte.dev/tutorial 中找到方便的一口大小的教程。

          我們希望在將 D3 與 JavaScript 框架或 Svelte 等編譯器相結(jié)合時將職責(zé)分開。該框架負(fù)責(zé)添加、刪除和操作 DOM 元素,而 D3 用于執(zhí)行與比例、形狀生成器、力布局等可視化相關(guān)的計算。簡而言之,您需要忘記數(shù)據(jù)綁定模式,并謹(jǐn)慎使用 D3 轉(zhuǎn)換以避免 D3 和 Svelte 之間的沖突。回到第8章,深入討論將D3與框架相結(jié)合的可能方法。

          若要開始處理本章的項目,請在代碼編輯器中打開 https://github.com/d3js-in-action-third-edition/code-files/tree/main/chapter_14/14.4.1-Responsive_SVG_container/start/src/data 的開始文件夾。打開集成終端并使用 npm install 安裝項目依賴項。然后用 npm run dev 啟動項目。該項目將在您的瀏覽器中提供,網(wǎng)址為 http://localhost:5173/ .您將在 src/ 文件夾中找到我們將處理的所有文件。

          • App.svelte 是我們項目的根組件,并調(diào)用創(chuàng)建頁面布局的子組件。
          • data/ 文件夾包含我們將用于此項目的四個數(shù)據(jù)集:drawings.json 、letters.json 、paintings.json 和 timeline.json 。
          • global_styles/ 包含四個 SCSS 文件,然后加載到 styles.scss 中。使用 SCSS 而不是 CSS 允許我們聲明樣式變量(例如 $text:#160E13 ),我們可以在整個項目中重用這些變量,使其更易于維護(hù)。
          • layout/ 包含項目布局的主要組件,如標(biāo)題和圖例。
          • chart_component/ 包含與特定可視化組件(如繪畫或字母)相關(guān)的組件。
          • UI/ 用于界面組件,如工具提示。
          • utils/ 包含實用程序函數(shù)和常量。

          14.4.1 響應(yīng)式SVG容器的另一種方法

          從第一章開始,我們采用了一種簡單而有效的方法來使SVG圖形響應(yīng):通過設(shè)置SVG容器的viewBox屬性并將寬度和高度屬性留空。這種方法非常容易實現(xiàn),并且開箱即用。唯一真正的缺點是,當(dāng) SVG 容器變小時,它包含的文本元素會按比例變小,使它們可能難以閱讀。

          在此項目中,我們將采用不同的方法,設(shè)置 SVG 容器的寬度和高度屬性并將 viewBox 留空。每當(dāng)屏幕尺寸發(fā)生變化時,我們將使用事件偵聽器來更新這些屬性。盡管這種方法需要我們作為程序員和瀏覽器付出更多的努力,但它使我們能夠根據(jù)屏幕寬度調(diào)整可視化的布局。此外,隨著屏幕尺寸的減小,它會保持文本標(biāo)簽的大小。

          在之前的項目討論中,我們決定顯示一個由小型多個可視化效果組成的網(wǎng)格。所有這些可視化將包含在單個 SVG 元素中。此外,我們將使用一個 12 列的 flexbox 網(wǎng)格,類似于第 9 章中討論的網(wǎng)格,用于包括時間軸和可視化效果在內(nèi)的整體頁面布局。

          在圖 14.8 中,您可以看到三種不同屏幕寬度的頁面布局:大于或等于 1400px(flexbox 網(wǎng)格容器的寬度)、小于 1400px 但大于 748px 和小于 748px。對于這三種屏幕尺寸中的每一種,SVG 容器寬度的計算略有不同。當(dāng)屏幕大于或等于 748px 時,時間線顯示在左側(cè),從 12 列網(wǎng)格中取出兩列,可視化效果或 SVG 容器顯示在右側(cè)剩余的十列上。當(dāng)屏幕小于 748px 時,我們會刪除時間線,可視化效果可以擴展到 12 列。彈性框網(wǎng)格容器還應(yīng)用了 30px 的填充到左側(cè)和右側(cè)。

          彈性框網(wǎng)格在其 CSS 屬性中的最大寬度限制為 1400 像素。這意味著即使在較大的屏幕上,內(nèi)容也不會超過此寬度。要計算寬度超過 1400px 的屏幕上 SVG 容器的寬度,我們可以減去 flexbox 網(wǎng)格容器兩側(cè)的填充,將其乘以 12,然后除以 <>。這是因為 SVG 容器跨越十二列中的十列。

          svg寬度=10/12 * (網(wǎng)格容器 - 2*填充)

          當(dāng)屏幕小于 1400 像素時,SVG 容器的大小會按比例變小。在 svgWidth 的方程中,我們只需要更改窗口寬度的 gridContainer。

          svg寬度=10/12 * (窗口寬度 - 2*填充)

          最后,當(dāng)屏幕小于 768px 時,SVG 容器將分布在屏幕的整個寬度減去填充。

          svg寬度=窗口寬度 - 2*填充

          圖 14.8 SVG 容器的寬度與屏幕寬度和頁面的響應(yīng)式布局成正比。在大于 748px 的屏幕上,SVG 分布在 12 列彈性框網(wǎng)格中的 12 列上,而在較小的屏幕上,它采用所有 <12> 列。


          在示例 14.2 中,我們使用這些方程來動態(tài)計算 SVG 容器的寬度。為此,我們在文件Grid.svelte中工作。我們首先聲明兩個變量,一個用于 windowWidth,一個用于 SVG width。使用 switch 語句,我們根據(jù)屏幕的寬度和剛才討論的方程設(shè)置 SVG width 變量的值。因為switch語句是用Svelte反應(yīng)符號($)聲明的,所以只要變量包含更改,它就會運行。

          請注意我們?nèi)绾螌?windowWidth 變量綁定到 window 對象的 innerWidth 屬性。在 Svelte 中,我們可以從任何組件訪問窗口對象 <svelte:window /> .

          最后,我們使用 svgWidth 變量動態(tài)設(shè)置 SVG 容器的 width 屬性。因為我們使用的是 JavaScript 框架,所以我們不使用 D3 將 SVG 元素附加到 DOM 中,而是直接將其添加到組件的標(biāo)記中。

          清單 14.2 動態(tài)更新 SVG 容器的寬度 (Grid.svelte)

          <script>
             let windowWidth;
             const gridContainer=1400;
             const padding=30;
             let svgWidth;
             $: switch (true) {                                           #A
                 case windowWidth >=gridContainer:                       #A
                   svgWidth=(10 / 12) * (gridContainer - 2 * padding);  #A
                   break;                                                 #A
                 case windowWidth < gridContainer && windowWidth >=768:  #A
                   svgWidth=(10 / 12) * (windowWidth - 2 * padding);    #A
                   break;                                                 #A
                 default:                                                 #A
                   svgWidth=windowWidth - 2 * padding;                  #A
                }                                                         #A
          </script>
           
          <svelte:window bind:innerWidth={windowWidth} />  #B
           
          <svg width={svgWidth} />  #C
           
          <style>
            svg {
              border: 1px solid magenta;
            }
          </style>

          在“樣式”部分中,已將洋紅色邊框添加到 SVG 元素中。您可以嘗試調(diào)整屏幕大小,以查看它如何影響 SVG 元素的寬度。

          響應(yīng)式 SVG 寬度

          現(xiàn)在處理了 SVG 容器的寬度,我們需要設(shè)置它的高度。由于 SVG 元素將包含一個由多個小型可視化組成的網(wǎng)格,因此如果我們知道:可視化的數(shù)量、它們的高度和網(wǎng)格中的列數(shù),我們可以計算它的高度。在示例 14.3 中,我們首先聲明了我們想要可視化梵高工作的年份數(shù)組。我們使用 D3 的范圍方法做到這一點。然后,我們根據(jù)屏幕的寬度設(shè)置網(wǎng)格的列數(shù)。如果屏幕大于 900px,我們需要三列,如果小于 600px,我們需要一列,在兩者之間,我們需要兩列。我們現(xiàn)在使用大概的數(shù)字,如果需要,我們會在以后進(jìn)行調(diào)整。

          一旦我們知道了列數(shù),我們就可以通過將小型多個可視化的數(shù)量除以列數(shù)并將結(jié)果四舍五入來計算行數(shù)。通過將 SVG 元素的寬度除以列數(shù)來找到每個小序列圖的寬度。我們還任意將它們的高度設(shè)置為寬度加 40px。最后,我們通過將行數(shù)乘以每個小倍數(shù)的高度來找到 SVG 元素的總高度。

          由于 svgWidth 和 svgHeight 變量在組件掛載時為 null,因此瀏覽器將引發(fā)錯誤。這就是為什么我們僅在定義了這兩個變量后才使用條件語句將 SVG 元素添加到標(biāo)記中。請注意 switch 語句和維度變量如何使用 $ 符號進(jìn)行響應(yīng)。每次屏幕寬度更改時,它們都會更新。

          我們有一個響應(yīng)式 SVG 元素!這個實現(xiàn)需要比我們以前的策略更多的工作,但在下一節(jié)中使用響應(yīng)式 SVG 網(wǎng)格時會很有幫助。

          示例 14.3 動態(tài)更新 SVG 容器的高度 (Grid.svelte)

          <script>
            import { range } from "d3-array";
           
            ...
           
            const years=range(1881, 1891);  #A
            let numColumns;                                  #B
            $: switch (true) {                               #B
              case windowWidth > 900:                        #B
                numColumns=3;                              #B
                break;                                       #B
              case windowWidth <=900 && windowWidth > 600:  #B
                numColumns=2;                              #B
                break;                                       #B
              default:                                       #B
                numColumns=1;                              #B
            }                                                #B
            $: numRows=Math.ceil(years.length / numColumns);  #C
            $: smWidth=svgWidth / numColumns;  #D
            $: smHeight=smWidth + 40;          #D
            $: svgHeight=numRows * smHeight;  #E
          </script>
           
          <svelte:window bind:innerWidth={windowWidth} />
           
          {#if svgWidth && svgHeight}                    #F
               <svg width={svgWidth} height={svgHeight} />  #F
          {/if}                                          #F
           
          <style>
            svg {
              border: 1px solid magenta;
            }
          </style>

          14.4.2 創(chuàng)建響應(yīng)式 SVG 網(wǎng)格

          在最后一個列表中,我們使用變量 smWidth 和 smHeight 確定每個網(wǎng)格項的寬度和高度。使用這些值,我們將構(gòu)建將保存所有可視化效果的網(wǎng)格。由于我們在 SVG 容器中工作,因此我們將使用組元素來包圍每個小倍數(shù)。

          首先,在清單 14.4 中,我們在 SVG 容器中插入一個 each 塊,用于遍歷先前創(chuàng)建的 years 數(shù)組。值得注意的是,我們可以訪問每年的索引(i)作為第二個參數(shù)。我們每年創(chuàng)建一個組元素,然后使用 transform 屬性應(yīng)用翻譯。為了確定每個組屬于哪一列,我們使用索引的余數(shù),也稱為模數(shù)(% ),除以列數(shù)。下面的等式說明了三列布局中介于 <> 和 <> 之間的索引的余數(shù)。然后,我們通過將余數(shù)乘以 smWidth 來計算水平平移。

          0 % 3=0

          1 % 3=1

          2 % 3=2

          3 % 3=0

          4 % 3=1

          5 % 3=2

          等等...

          對于垂直平移,我們將索引四舍五入除以列數(shù),以了解我們在哪一行,然后將結(jié)果乘以網(wǎng)格元素的高度。然后,我們在組中附加一個矩形元素,將其尺寸設(shè)置為網(wǎng)格項的寬度和高度,并為其提供藍(lán)色筆觸。我們添加此矩形以確保網(wǎng)格按預(yù)期工作,并在屏幕寬度更改時正確調(diào)整大小,但我們不會將其保留在最終可視化效果中。

          示例 14.4 向 SVG 容器添加響應(yīng)式網(wǎng)格 (Grid.svelte)

          {#if svgWidth && svgHeight}
            <svg width={svgWidth} height={svgHeight}>
              {#each years as year, i}  #A
                <g transform="translate(                     #B
                  {(i % numColumns) * smWidth},              #B
                  {Math.floor(i / numColumns) * smHeight})"  #B
                >                                            #B
                  <rect x={0} y={0} width={smWidth} height={smHeight} />  #C
                </g>
              {/each}
            </svg>
          {/if}
           
          <style>
            svg {
              border: 1px solid magenta;
            }
            rect {
              fill: none;
              stroke: cyan;
            }
          </style>

          實現(xiàn)網(wǎng)格后,調(diào)整屏幕大小以確保網(wǎng)格項的列數(shù)和位置按預(yù)期調(diào)整。當(dāng)屏幕大于 900px 時,網(wǎng)格應(yīng)有三列,600 到 900px 之間應(yīng)有兩列,如果小于 600px,則有一列,如圖 14.9 所示。

          圖 14.9 SVG 網(wǎng)格在大于 900 像素的屏幕上有三列,在 600 到 900 像素之間的屏幕上有兩列,在較小的屏幕上有一列。


          響應(yīng)式 SVG 網(wǎng)格

          14.5 創(chuàng)建徑向可視化

          準(zhǔn)備好項目骨架后,我們可以開始利用 D3 來創(chuàng)建梵高作品的可視化!在本節(jié)中,我們將構(gòu)建我們的小的多重可視化,從軸和標(biāo)簽開始,繼續(xù)繪畫,最后是繪圖和字母。

          14.5.1 添加徑向軸

          我們的小型多重可視化的主干可以簡化為背景圓圈和年份標(biāo)簽。但在實施這些元素之前,我們需要定義它們的確切位置。圖 14.10 顯示了在定位圓圈和年份標(biāo)簽之前需要考慮的不同參數(shù)的草圖。我們已經(jīng)計算了每個小倍數(shù)的寬度(smWidth)和高度(smHeight)。為了確保可視化之間有足夠的空間并為月份標(biāo)簽留出空間,我們可以定義要在每個圓圈周圍應(yīng)用的填充,比如說 60px。根據(jù)這個值和網(wǎng)格元素的寬度,我們可以計算背景圓的半徑。

          圖 14.10 我們已經(jīng)計算了網(wǎng)格元素的寬度和高度。如果我們在背景圓圈周圍設(shè)置一個固定的填充值,我們可以計算它們的半徑。


          我們將開始在 Grid.svelte 的子組件中構(gòu)建可視化,名為 GridItem.svelte 。在清單 14.5 中,我們首先將此組件導(dǎo)入到 Grid.svelte 中。然后,我們將 GridItem 附加到每個塊中,這將從年份數(shù)組中生成每年的 GridItem。我們將 smWidth 、smHeight 和當(dāng)前年份作為道具傳遞給這個子組件。

          清單 14.5 導(dǎo)入 GridItem 組件 (Grid.svelte)

          <script>
            import GridItem from "./GridItem.svelte";  #A
           
            ...
          </script>
           
          {#if svgWidth && svgHeight}
            <svg width={svgWidth} height={svgHeight}>
              {#each years as year, i}
                <g transform="translate(
                     {(i % numColumns) * smWidth},
                     {Math.floor(i / numColumns) * smHeight})"
                >
                  <rect x={0} y={0} width={smWidth} height={smHeight} />
                  <GridItem {smWidth} {smHeight} {year} />  #B
                </g>
              {/each}
            </svg>
          {/if}

          在清單 14.6 中,我們開始在 GridItem.svelte 中工作。我們在腳本標(biāo)簽中導(dǎo)入道具 smWidth , smHeight 和 year。然后,我們將填充常量設(shè)置為值 60,并根據(jù)填充和 smWidth 計算圓的半徑。因為半徑被聲明為一個反應(yīng)變量 ($),所以只要 smWidth 發(fā)生變化,它就會被重新計算。

          在標(biāo)記中,我們使用兩個組元素來設(shè)置可視化的相對坐標(biāo)系的原點。第一個水平轉(zhuǎn)換為半 smWidth .它用作年份標(biāo)簽的參考點,然后只需將其垂直平移到網(wǎng)格項的底部。第二組元素垂直平移到背景圓的中心。當(dāng)我們開始向可視化追加其他形狀以表示繪畫、素描和字母時,此策略將特別方便。

          示例 14.6 將背景圓圈和年份標(biāo)簽添加到可視化中 (GridItem.svelte)

          <script>
            export let smWidth;   #A
            export let smHeight;  #A
            export let year;      #A
           
            const padding=60;                       #B
            $: radius=(smWidth - 2 * padding) / 2;  #B
          </script>
           
          <g transform="translate({smWidth / 2}, 0)">         #C
            <g transform="translate(0, {padding + radius})">  #C
              <circle cx={0} cy={0} r={radius} />
            </g>
            <text x={0} y={smHeight - 5} text-anchor="middle">{year}</text>
          </g>
           
          <style lang="scss">
            circle {
              fill: none;
              stroke: $text;
            }
          </style>

          下一步是為每個月添加一個軸和標(biāo)簽,如圖 14.10 所示。此圖顯示圓坐標(biāo)系的原點位于其中心,這要歸功于之前翻譯的 SVG 組。每個月的軸將是一條從原點開始并到達(dá)圓周的線,每個月的角度都不同。

          為了計算軸端點的位置,我們需要做一些三角函數(shù)。讓我們以二月的軸為例。在圖 14.11 的右側(cè),您可以看到我們可以通過將軸與其水平 (x) 和垂直 (y) 邊長連接起來來形成一個直角三角形(其中一個角為 90° 角的三角形)。我們也可以稱θ(θ)為12點鐘位置(零度時)與二月軸之間的角度。

          三角函數(shù)告訴我們,θ 的正弦等于 x 除以二月軸的長度或背景圓的半徑。因此,我們可以通過將半徑乘以sinθ來計算端點的水平位置。類似地,θ 的余弦等于 y 除以二月軸的長度。因此,我們可以通過將半徑乘以 cosθ 和 -1 來計算端點的垂直位置,因為我們正朝著垂直軸的負(fù)方向前進(jìn)。

          sinθ=x / 半徑=> x=半徑 * sinθ

          余量θ=y / 半徑=> y=半徑 * 余量θ

          圖 14.11 我們想在圓圈內(nèi)為每個月畫一個軸。這些軸的起點是可視化坐標(biāo)系的原點,而端點的位置可以通過基本三角法確定。


          為了繪制月份軸,我們繼續(xù)在 網(wǎng)格項目.svelte .我們首先聲明一個點刻度,該刻度將月份數(shù)組作為輸入(該數(shù)組在文件 /utils/months.js 中可用)并返回相應(yīng)的角度。我們希望 12 月顯示在 360 點鐘位置,對應(yīng)于零角度。我們知道,一個完整的圓覆蓋 2° 或 2π 弧度。因為一年有十二個月,所以我們將刻度中的最后一個角度設(shè)置為 2π - 12π/<> 弧度,或一個完整的圓減去十二分之一的圓。

          在標(biāo)記中,我們使用每個塊為每個月附加一個行元素。每條線的起點是 (0, 0) ,而它的端點是用剛才討論的三角函數(shù)計算的。

          示例 14.7 添加月份軸 (GridItem.svelte)

          <script>
            import { scalePoint } from "d3-scale";
            import { months } from "../utils/months";
           
            export let smWidth;
            export let smHeight;
            export let year;
           
            const padding=60;
            $: radius=(smWidth - 2 * padding) / 2;
           
            const monthScale=scalePoint()                   #A
              .domain(months)                                 #A
              .range([0, 2 * Math.PI - (2 * Math.PI) / 12]);  #A
          </script>
           
           
          <g transform="translate({smWidth / 2}, 0)">
            <g transform="translate(0, {padding + radius})">
              <circle cx={0} cy={0} r={radius} />
              {#each months as month}                             #B
                <line                                             #B
                  x1="0"                                          #B
                  y1="0"                                          #B
                  x2={radius * Math.sin(monthScale(month))}       #B
                     y2={-1 * radius * Math.cos(monthScale(month))}  #B
                  stroke-linecap="round"                          #B
                />                                                #B
              {/each}                                             #B
            </g>
          <text x={0} y={smHeight - 5} text-anchor="middle">{year}</text>
          </g>
           
          <style lang="scss">
            circle {
              fill: none;
              stroke: $text;
            }
            line {
              stroke: $text;
              stroke-opacity: 0.2;
            }
          </style>

          作為最后一步,我們要在每個月的軸上添加一個標(biāo)簽,在圓圈外 30px。在示例 14.8 中,我們?yōu)槊總€月附加一個文本元素,并使用 JavaScript slice() 方法將文本設(shè)置為該月的前三個字母。為了正確定位文本標(biāo)簽,我們執(zhí)行翻譯,然后旋轉(zhuǎn)。我們發(fā)現(xiàn)帶有三角函數(shù)的平移,類似于我們計算軸端點的方式。對于圓圈上半部分(9 點鐘和 3 點鐘之間)顯示的標(biāo)簽,旋轉(zhuǎn)與其軸相同。對于下半部分(3點鐘和9點鐘之間)的標(biāo)簽,我們給它們額外的180°旋轉(zhuǎn),以便它們更容易閱讀。

          雖然我們在 JavaScript Math.sin() 和 Math.cos() 函數(shù)中使用弧度,但旋轉(zhuǎn)的轉(zhuǎn)換屬性需要度數(shù)。為了便于從弧度到度數(shù)的轉(zhuǎn)換,我們創(chuàng)建了一個名為 radiansToDegrees() 的輔助函數(shù),您可以在 /utils/helper.js 中找到該函數(shù)。它采用弧度的角度作為輸入,并返回相同的弧度角度。

          清單 14.8 添加月份標(biāo)簽 (GridItem.svelte)

          <script>
            import { scalePoint } from "d3-scale";
            import { months } from "../utils/months";
            import { radiansToDegrees } from "../utils/helpers";
           
            export let smWidth;
            export let smHeight;
            export let year;
           
            const padding=60;
            $: radius=(smWidth - 2 * padding) / 2;
           
            const monthScale=scalePoint()
              .domain(months)
              .range([0, 2 * Math.PI - (2 * Math.PI) / 12]);
          </script>
           
          <g transform="translate({smWidth / 2}, 0)">
            <g transform="translate(0, {padding + radius})">
              <circle cx={0} cy={0} r={radius} />
              {#each months as month}
                <line
                  x1="0"
                  y1="0"
                  x2={radius * Math.sin(monthScale(month))}
                     y2={-1 * radius * Math.cos(monthScale(month))}
                  stroke-linecap="round"
                />
                <text  #A
                  class="month-label"
                  transform="translate(                                            #B
                               {(radius + 30) * Math.sin(monthScale(month))},      #B   
                               {-1 * (radius + 30) * Math.cos(monthScale(month))}  #B
                             )                                                     #B
                             rotate({                                         #C
                               monthScale(month) <=Math.PI / 2 ||            #C
                               monthScale(month) >=(3 * Math.PI) / 2         #C
                                 ? radiansToDegrees(monthScale(month))        #C
                                 : radiansToDegrees(monthScale(month)) - 180  #C
                             })"                                              #C
                  text-anchor="middle"
                  dominant-baseline="middle">{month.slice(0, 3)}</text  #D
                >
              {/each}
            </g>
          <text x={0} y={smHeight - 5} text-anchor="middle">{year}</text>
          </g>
           
          <style lang="scss">
            circle {
              fill: none;
              stroke: $text;
            }
            line {
              stroke: $text;
              stroke-opacity: 0.2;
            }
            .month-label {
              font-size: 1.4rem;
            }
          </style>

          14.5.2 在圓周上應(yīng)用力布局

          準(zhǔn)備好軸后,我們可以開始繪制可視化效果了。我們將首先用一個圓圈來表示梵高的每幅畫。這些圓圈將圍繞相應(yīng)月份軸的端點按年份分組,然后按創(chuàng)建月份分組。如第14.3節(jié)所述,圓圈的顏色將基于繪畫的主題及其在媒介上的邊框。最后,我們將圓的面積設(shè)置為與相關(guān)圖稿的尺寸成比例。圖 14.12 顯示了我們所追求的效果。為了在避免重疊的同時生成圓簇,我們將使用 D3 的力布局。

          圖14.12 梵高創(chuàng)作的每幅畫都將用一個圓圈表示。這些圓圈將定位在與其創(chuàng)建年份相對應(yīng)的小倍數(shù)可視化上,聚集在月軸的頂端。我們將使用 D3 的力布局來創(chuàng)建這些集群。


          在進(jìn)一步進(jìn)入代碼之前,讓我們花點時間思考一下組件的體系結(jié)構(gòu),并制定最佳前進(jìn)方向的戰(zhàn)略。我們的小倍數(shù)可視化由三層組件組成,如圖 14.13 所示。第一個由 Grid.svelte 組件持有,該組件負(fù)責(zé)將 SVG 容器添加到標(biāo)記中,并將年份分解為類似網(wǎng)格的布局。該組件“知道”我們將生成可視化的所有年份。

          第二層由 GridItem.svelte 處理。此組件僅“感知”單個年份的數(shù)據(jù),并顯示其相應(yīng)的年份標(biāo)簽和月份軸。最后,還有組件 繪畫.svelte , 圖紙.svelte 和 字母.svelte 。我們還沒有處理這些文件,但它們包含在 chart_components/ 文件夾中。顧名思義,這些組件負(fù)責(zé)可視化一年中產(chǎn)生的繪畫、素描和信件。因為它們將從GridItem.svelte調(diào)用,所以它們也將知道一年的數(shù)據(jù)。

          圖 14.13 我們的小型多重可視化涉及三層組件。第一個(Grid.svelte)負(fù)責(zé)整體可視化及其響應(yīng)網(wǎng)格。第二個 (GridItem.svelte) 保存每個小的多重可視化效果(對應(yīng)于一年),并顯示月份軸。最后一個(Paintings.svelte,Drawings.svelte和Letters.svelte)負(fù)責(zé)與繪畫,繪圖和字母相關(guān)的可視化元素。



          考慮到這種分層架構(gòu),我們看到加載整個繪畫數(shù)據(jù)集的最佳位置是 Grid.svelte ,因為該組件監(jiān)督可視化的整體性,并且在應(yīng)用程序中僅加載一次。然后,該組件會將每年對應(yīng)的繪畫作為道具傳遞給GridItem.svelte,然后將它們傳遞給Paintings.svelte。

          基于這個邏輯,在清單 14.9 中,我們回到 Grid.svelte 并導(dǎo)入繪畫數(shù)據(jù)集。由于我們稍后希望根據(jù)繪畫的尺寸調(diào)整代表繪畫的圓圈的大小,因此我們計算這些作品的面積,并使用此信息查找數(shù)據(jù)集中最大的繪畫尺寸。請注意,數(shù)據(jù)集中有一些繪畫的尺寸不可用。在這種情況下,我們將相應(yīng)圓的半徑設(shè)置為 3px。

          要將繪畫的面積(以cm 2為單位)縮放為屏幕上的圓形區(qū)域(以px2為單位),我們可以使用線性比例。我們稱此比例為繪畫AreaScale,并使用圓的面積公式找到范圍覆蓋的最大面積:

          a=πr2

          最后,我們將顯示繪畫所需的數(shù)據(jù)和函數(shù)傳遞給 GridItem .請注意我們?nèi)绾芜^濾繪畫數(shù)據(jù)集以僅傳遞與當(dāng)前年份對應(yīng)的繪畫。

          示例 14.9 加載繪畫數(shù)據(jù)集并為繪畫區(qū)域創(chuàng)建比例 (Grid.svelte)

          <script>
            import { range, max } from "d3-array";
            import { scaleLinear } from "d3-scale";
            import paintings from "../data/paintings.json";
           
            ...
           
            paintings.forEach((painting)=> {                                   #A
              if (painting.width_cm && painting.height_cm) {                    #A
                painting["area_cm2"]=painting.width_cm * painting.height_cm;  #A
              }                                                                 #A
            });                                                                 #A
            const maxPaintingArea=max(paintings, (d)=> d.area_cm2);          #A
           
            const maxPaintingRadius=8;                              #B
            const paintingDefaultRadius=3;                          #B
            const paintingAreaScale=scaleLinear()                   #B
              .domain([0, maxPaintingArea])                           #B
              .range([0, Math.PI * Math.pow(maxPaintingRadius, 2)]);  #B
          </script>
           
          <svelte:window bind:innerWidth={windowWidth} />
           
          {#if svgWidth && svgHeight}
            <svg width={svgWidth} height={svgHeight}>
              {#each years as year, i}
                <g
                  transform="translate(
                     {(i % numColumns) * smWidth},
                     {Math.floor(i / numColumns) * smHeight})"
                >
                  <rect x={0} y={0} width={smWidth} height={smHeight} />
                  <GridItem
                    {smWidth}
                    {smHeight}
                    {year}
                    {paintingAreaScale}                        #C
                    {paintingDefaultRadius}                    #C
                    paintings={paintings.filter((painting)=>  #C
                      painting.year===year)}                 #C
                  />
                </g>
              {/each}
            </svg>
          {/if}

          在 GridItem.svelte 中,我們所要做的就是聲明從 Grid.svelte 接收的道具,導(dǎo)入 Paintings 組件,將 Paintings 組件添加到標(biāo)記中,然后傳遞相同的 props,如清單 14.10 所示。

          示例 14.10 導(dǎo)入繪畫組件并再次傳遞道具 (GridItem.svelte)

          <script>
            import Paintings from "../chart_components/Paintings.svelte";  #A
           
            export let paintingAreaScale;      #B
            export let paintingDefaultRadius;  #B
            export let paintings;              #B
           
            ...
          </script>
           
          <g transform="translate({smWidth / 2}, 0)">
            <g transform="translate(0, {padding + radius})">
              <circle ... />
              {#each months as month}
                 <line ... />
                 <text ... >{month.slice(0, 3)}</text>
              {/each}
              <Paintings                 #C
                {paintingAreaScale}      #C
                {paintingDefaultRadius}  #C
                {paintings}              #C
                {monthScale}             #C
                {radius}                 #C
              />                         #C
            </g>
            <text ... >{year}</text>
          </g>

          最后,真正的動作發(fā)生在《畫畫》中。現(xiàn)在,我們循環(huán)瀏覽作為道具收到的畫作,并在每幅畫的標(biāo)記中添加一個圓圈。這些圓的初始位置是它們相關(guān)月份軸的尖端,這可以通過我們之前使用的三角函數(shù)找到。我們還必須考慮我們不知道它們是在哪個月創(chuàng)作的畫作。我們將它們放置在可視化效果的中心。

          為了計算圓的半徑,我們稱之為 繪畫面積比例 .由于此刻度返回一個面積,因此我們需要使用以下公式計算相應(yīng)的半徑:

          r=√(a/π)

          14.11 為每幅畫附加一個圓圈(Paintings.svelte)

          <script>
            export let paintingAreaScale;      #A
            export let paintingDefaultRadius;  #A
            export let paintings;              #A
            export let monthScale;             #A
            export let radius;                 #A
          </script>
           
          {#each paintings as painting}  #B
            <circle                      #B
              cx={painting.month !==""                                #C
                 ? radius * Math.sin(monthScale(painting.month))       #C
                 : 0}                                                  #C
              cy={painting.month !==""                                #C
                 ? -1 * radius * Math.cos(monthScale(painting.month))  #C 
                 : 0}                                                  #C
              r={painting.area_cm2                                            #D
                 ? Math.sqrt(paintingAreaScale(painting.area_cm2) / Math.PI)  #D
                 : paintingDefaultRadius}                                     #D
            />                                                                #D
          {/each}

          在這個階段,繪畫的圓圈在其月軸的頂端重疊,如圖 14.14 所示。我們將在一分鐘內(nèi)通過 D3 的力布局解決這個問題。

          圖 14.14 在這個階段,繪畫的圓圈在其月軸的頂端重疊。我們將通過 D3 的力布局來解決這個問題。



          為了在每個月軸的尖端創(chuàng)建節(jié)點集群,我們將使用 D3 的力布局。這種布局有點復(fù)雜,所以如果你需要更深入的介紹,我們建議閱讀第12章。在示例 14.12 中,我們使用 forceSimulation() 方法初始化一個新的力模擬,我們將繪畫數(shù)組傳遞給該方法。我們還聲明了一個空節(jié)點數(shù)組,在每次報價后,我們使用模擬的節(jié)點進(jìn)行更新。然后,我們遍歷此節(jié)點數(shù)組而不是繪畫,以將圓圈附加到標(biāo)記中。

          我們計算施加在反應(yīng)塊($ )內(nèi)節(jié)點的力,以便在相關(guān)變量發(fā)生變化時觸發(fā)重新計算。在這個塊內(nèi),定位力(forceX和forceY)將節(jié)點推向其月軸的尖端,而碰撞力(forceCollide)確保節(jié)點之間沒有重疊。

          我們還降低了 alpha(模擬的“溫度”)并提高了 alpha 衰減率,以幫助模擬更快地收斂。這種調(diào)整需要反復(fù)試驗的方法才能找到正確的設(shè)置。

          最后,我們使用仿真添加到節(jié)點的 x 和 y 屬性來設(shè)置相應(yīng)圓的 cx 和 cy 屬性。

          14.12 使用力布局計算每幅畫的位置(Paintings.svelte)

          <script>
            import { forceSimulation, forceX, forceY, forceCollide } from "d3-force";
           
            ...
           
            let simulation=forceSimulation(paintings);  #A
            let nodes=[];                               #A
            simulation.on("tick", ()=> {  #B
              nodes=simulation.nodes();  #B
            });                            #B
           
            $: {          #C
              simulation  #C
                .force("x",                                        #D
                  forceX((d)=> d.month !==""                     #D
                    ? radius * Math.sin(monthScale(d.month))       #D
                    : 0                                            #D
                  ).strength(0.5)                                  #D
                )                                                  #D
                .force("y",                                        #D
                  forceY((d)=> d.month !==""                     #D
                    ? -1 * radius * Math.cos(monthScale(d.month))  #D
                    : 0                                            #D
                  ).strength(0.5)                                  #D
                )                                                  #D
                .force("collide",                                               #E
                  forceCollide()                                                #E
                    .radius((d)=> d.width_cm===null && d.height_cm===null  #E
                      ? paintingDefaultRadius + 1                               #E
                      : Math.sqrt(paintingAreaScale(d.area_cm2) / Math.PI) + 1  #E
                  ).strength(1)                                                 #E
                )                                                               #E
                .alpha(0.5)        #F
                .alphaDecay(0.1);  #F
            }
          </script>
           
          {#each nodes as node}  
            <circle              
              cx={node.x}  #G
              cy={node.y}  #G
              r={node.area_cm2
                 ? Math.sqrt(paintingAreaScale(node.area_cm2) / Math.PI)
                 : paintingDefaultRadius}
            />
          {/each}

          現(xiàn)在,您應(yīng)該會看到節(jié)點群集出現(xiàn)在月份軸的提示處。為了完成繪畫可視化,我們將根據(jù)圓圈相應(yīng)繪畫的主題設(shè)置圓圈的顏色。該文件實用程序/主題.js包含可用的繪畫主題及其顏色的數(shù)組。在示例 14.13 中,我們聲明了一個序數(shù)尺度,它將主題作為輸入并返回相應(yīng)的圓圈。然后,我們所要做的就是通過調(diào)用此刻度來設(shè)置圓圈的填充屬性。

          14.13 根據(jù)圓圈對應(yīng)的繪畫主題設(shè)置圓圈的顏色(Paintings.svelte)

          <script>
            import { scaleOrdinal } from "d3-scale";
            import { subjects } from "../utils/subjects";
           
            ...
           
            const colorScale=scaleOrdinal()          #A
              .domain(subjects.map((d)=> d.subject))  #A
              .range(subjects.map((d)=> d.color));    #A
          </script>
           
          {#each nodes as node}
            <circle
              cx={node.x}
              cy={node.y}
              r={node.area_cm2
                 ? Math.sqrt(paintingAreaScale(node.area_cm2) / Math.PI)
                 : paintingDefaultRadius}
              fill={colorScale(node.subject)}  #B
            />
          {/each}

          我們已經(jīng)完成了對繪畫的可視化!此時,您的可視化效果將類似于圖 14.15 中的可視化效果。

          圖14.15 梵高在1887年間創(chuàng)作的畫作的可視化。


          14.5.3 繪制徑向面積圖

          我們的下一步是繪制一個面積圖,可視化梵高每年完成的繪畫數(shù)量。在第 4 章中,我們學(xué)習(xí)了如何使用 D3 的形狀生成器來計算折線圖和面積圖路徑元素的 d 屬性。在這里,我們將使用與形狀生成器 lineRadial() 類似的策略,該策略在 d3 形狀模塊中可用。

          與上一節(jié)一樣,我們希望考慮用于渲染可視化的三層 Svelte 組件。我們將在 Grid.svelte 中加載整個圖紙數(shù)據(jù)集,并計算一個月的最大圖紙數(shù)量。我們還將重新組織數(shù)據(jù)集以每年拆分信息,如清單 14.14 所示。我們將此信息傳遞給 GridItem.svelte,并初始化一個刻度,負(fù)責(zé)計算與許多繪圖對應(yīng)的沿月軸的徑向位置(參見示例 14.15),并將所有這些信息傳遞給 Drawings.svelte,后者將繪制面積圖。

          清單 14.14 輸入圖形并重新組織數(shù)據(jù) (Grid.svelte)

          <script>
            import drawings from "../data/drawings.json";
            import { months } from "../utils/months";
           
            ...
           
            const yearlyDrawings=[];                             #A
            years.forEach((year)=> {                              #A
              const relatedDrawings={ year: year, months: [] };  #A
              months.forEach((month)=> {                          #A
                relatedDrawings.months.push({                      #A
                  month: month,                                    #A
                  drawings: drawings.filter(drawing=>             #A
                    drawing.year===year.toString() &&            #A
                    drawing.month===month),                      #A
                });                                                #A
              });                                                  #A
              yearlyDrawings.push(relatedDrawings);                #A
            });                                                    #A
           
            const maxDrawings=max(yearlyDrawings, d=>  #B
              max(d.months, (i)=> i.drawings.length)     #B
            );                                            #B
          </scrip>
           
          <svelte:window bind:innerWidth={windowWidth} />
           
          {#if svgWidth && svgHeight}
            <svg width={svgWidth} height={svgHeight}>
              {#each years as year, i}
                <g
                  transform="translate(
                     {(i % numColumns) * smWidth},
                     {Math.floor(i / numColumns) * smHeight})"
                >
                  <rect x={0} y={0} width={smWidth} height={smHeight} />
                  <GridItem
                    {smWidth}
                    {smHeight}
                    {year}
                    {paintingAreaScale}                       
                    {paintingDefaultRadius}                   
                    paintings={paintings.filter((painting)=> 
                      painting.year===year)}
                    {maxDrawings}                                                  #C
                    drawings={yearlyDrawings.find((d)=> d.year===year).months}  #C                
                  />
                </g>
              {/each}
            </svg>
          {/if}

          示例 14.15 初始化一個刻度,負(fù)責(zé)沿月軸定位繪圖數(shù)量 (GridItem.svelte)

          <script>
            import Drawings from "../chart_components/Drawings.svelte";
           
            export let maxDrawings;
            export let drawings;
           
            ...
           
           $: radialScale=scaleLinear()  #A
             .domain([0, maxDrawings])     #A
             .range([0, 2 * radius]);      #A
          </script>
           
          <g transform="translate({smWidth / 2}, 0)">
            <g transform="translate(0, {padding + radius})">
              <circle ... />
              ...
              <Drawings {drawings} {monthScale} {radialScale} /> #B
            </g>
            <text ... >{year}</text>
          </g>

          在清單 14.16 中,我們使用 D3 的 lineRadial() 方法來初始化一個線生成器。如第 4 章所述,我們設(shè)置其訪問器函數(shù)來計算每個數(shù)據(jù)點的位置。但是這一次,我們使用的是極坐標(biāo)而不是笛卡爾坐標(biāo),因此有必要使用 angle() 和 radius() 函數(shù)。當(dāng)我們將 path 元素附加到標(biāo)記時,我們調(diào)用行生成器來設(shè)置其 d 屬性。在樣式中,我們給它一個半透明的填充屬性。

          在14.16 繪制徑向面積圖

          <script>
            import { lineRadial, curveCatmullRomClosed } from "d3-shape";
           
            export let drawings;
            export let monthScale;
            export let radialScale;
           
            const lineGenerator=lineRadial()                #A
              .angle((d)=> monthScale(d.month))              #A
              .radius((d)=> radialScale(d.drawings.length))  #A
              .curve(curveCatmullRomClosed);                  #A
          </script>
           
          <path d={lineGenerator(drawings)} />  #B
           
          <style lang="scss">
            path {
              fill: rgba($secondary, 0.25);
              pointer-events: none;
            }
          </style>

          圖14.16顯示了1885年的面積圖。

          圖 14.16 D3的徑向線生成器用于繪制梵高每年創(chuàng)作的圖紙數(shù)量的徑向面積圖。


          14.5.4 繪制徑向條形圖

          我們將可視化梵高作品的最后一部分是他每個月寫的信的數(shù)量。因為您現(xiàn)在擁有所有必需的知識,所以請自己試一試!

          練習(xí):用徑向條形圖可視化梵高每月寫的字母數(shù)量

          您可以自己完成此項目,也可以按照以下說明進(jìn)行操作:

          1. 在 Grid.svelte 中加載字母數(shù)據(jù)集。此數(shù)據(jù)集包含每月寫的信件總數(shù)。

          2. 通過道具將當(dāng)前年份對應(yīng)的字母傳遞給 GidItem.svelte。

          3. 在 GidItem.svelte 中,導(dǎo)入字母組件。將其添加到標(biāo)記中,并將字母數(shù)據(jù)和比例作為道具傳遞。

          4. 在 Letters.svelte 中,為每個月附加一行,然后根據(jù)相關(guān)字母的數(shù)量并使用三角函數(shù)設(shè)置行的端點。

          梵高在1885年間寫的信數(shù)量。



          如果您在任何時候遇到困難或想將您的解決方案與我們的解決方案進(jìn)行比較,您可以在附錄 D 的 D.14 節(jié)和本章代碼文件的文件夾 14.5.5-Radial_bar_chart / 末尾找到它。但是,像往常一樣,我們鼓勵您嘗試自己完成它。您的解決方案可能與我們的略有不同,沒關(guān)系!

          為了完成可視化的靜態(tài)版本,我們注釋掉之前用于查看網(wǎng)格布局的矩形和圓形并添加時間線。由于時間軸與 D3 沒有太大關(guān)系,因此我們不會解釋代碼,但您可以在附錄 D 的清單 D.14.4 和本章代碼文件的文件夾 14.5.4 中找到它。您也可以將其視為自己構(gòu)建的挑戰(zhàn)!帶有時間軸的完整靜態(tài)布局如圖 14.7 所示。

          圖 14.17 完成的靜態(tài)布局包括梵高在 1881 年至 1890 年間居住的時間線,以及他每年可視化的藝術(shù)作品。


          14.6 規(guī)劃有意義的交互

          現(xiàn)在我們的靜態(tài)項目已經(jīng)準(zhǔn)備就緒,必須退后一步,考慮未來的用戶可能想要如何探索它。他們將尋找哪些其他信息?他們會問哪些問題?我們可以通過互動來回答這些問題嗎?以下是用戶可能會提出的三個問題示例:

          • 每個圓圈代表哪幅畫?我們能看到嗎?
          • 我怎么知道 1885 年 <> 月制作了多少圖紙和信件?目前,我可以用圖例估計值,但看到數(shù)字會更好。
          • 我怎樣才能將時間軸中顯示的梵高居住的城市與他的藝術(shù)作品聯(lián)系起來?

          我們可以通過簡單的交互來回答這些問題:前兩個帶有工具提示,最后一個帶有交叉突出顯示。由于本章已經(jīng)很長了,并且此類交互與 D3 無關(guān)(在框架中,我們傾向于避免使用 D3 的事件偵聽器,因為我們不希望 D3 與 DOM 交互),因此我們不會詳細(xì)介紹如何實現(xiàn)它們。本節(jié)的主要重點是為您提供一個示例,說明如何規(guī)劃對項目有意義的交互。您可以在在線托管項目 (https://d3js-in-action-third-edition.github.io/van_gogh_work/) 上使用這些交互,并在本章代碼文件的文件夾 14.6 中找到代碼。下圖也顯示了它們的實際效果。

          圖 14.18 當(dāng)鼠標(biāo)位于繪畫的圓圈上并顯示此繪畫的細(xì)節(jié)時觸發(fā)的工具提示。



          圖 14.19 當(dāng)鼠標(biāo)位于可視化效果上并顯示每個月繪畫、素描和字母的數(shù)量時觸發(fā)的工具提示。


          圖 14.20 在時間軸上選擇某個時間段時,可視化中僅顯示該時間段內(nèi)創(chuàng)建的繪畫。


          我們的項目到此結(jié)束!我們希望它能激發(fā)您對可視化的創(chuàng)意。如果您想更深入地了解將 D3 與 Svelte 相結(jié)合以實現(xiàn)交互式數(shù)據(jù)可視化,我們強烈推薦 Connor Rothschild 的 Svelte 更好的數(shù)據(jù)可視化課程 (https://www.newline.co/courses/better-data-visualizations-with-svelte)。

          14.7 小結(jié)

          • D3的主要賣點之一是它如何使我們能夠創(chuàng)建創(chuàng)新的可視化。
          • 在處理可視化項目時,我們傾向于遵循以下步驟:收集數(shù)據(jù)、清理和探索數(shù)據(jù)、繪制可視化布局、構(gòu)建項目骨架、實現(xiàn)可視化元素以及添加交互。
          • 我們可以使用我們的JavaScript技能從網(wǎng)頁中提取數(shù)據(jù)。
          • 在探索數(shù)據(jù)時,列出我們可以使用的定量和定性數(shù)據(jù)屬性很有幫助,因為我們使用不同的渠道來可視化它們。
          • 為了創(chuàng)建創(chuàng)新的數(shù)據(jù)可視化,我們需要將所需的視覺通道分解為構(gòu)建塊。了解哪些 D3 模塊包含實現(xiàn)這些塊所需的方法非常有用。
          • 創(chuàng)建徑向可視化時,我們在極坐標(biāo)系中工作。我們可以使用基本的三角函數(shù)來計算不同可視化元素的位置。
          • 若要規(guī)劃有意義的交互,請問問自己用戶在瀏覽可視化時想要回答哪些問題。

          篇文章就來介紹下如何使用 vue3 + ts + svg + ECharts 實現(xiàn)一個如下所示的雙十一數(shù)據(jù)大屏頁面:

          創(chuàng)建項目

          執(zhí)行命令 npm create vue@latest 創(chuàng)建基于 Vite 構(gòu)建的 vue3 項目,功能選擇如下:

          我選擇使用 pnpm 安裝項目依賴:pnpm i,各安裝包的版本號可見于下圖:

          在 vite.config.ts 中添加配置,以便在項目啟動時能自動打開瀏覽器:

          typescript

          復(fù)制代碼

          export default defineConfig({ // ... server: { open: true } })

          現(xiàn)在,就可以通過 pnpm dev 啟動新建的項目了。

          大屏適配

          大屏適配的方案有很多,比如 rem、vw 和 flex 布局等,我選擇使用縮放(scale)的方式來適配大屏,因為該方案使用起來比較簡單,也不用考慮第三方庫的單位等問題。

          假設(shè)設(shè)計稿的尺寸為 1920 * 1080px,為了保證效果,在大屏中放大時應(yīng)該保持寬高比 designRatio 不變,designRatio 為 1920 / 1080 ≈ 1.78。放大的倍數(shù) scaleRatio,可以分為以下 2 種情況計算:

          • 當(dāng)用于展示的設(shè)備屏幕的寬高比 deviceRatio 等于或小于設(shè)計稿的寬高比 designRatio 時,我們可以按照兩者的寬度之比進(jìn)行縮放,即讓設(shè)計稿保持寬高比的情況下放大到與設(shè)備等寬,在高度上可能留有空白;
          • 當(dāng)設(shè)備屏幕的寬高比 deviceRatio 大于設(shè)計稿寬高比 designRatio 時,也就是說設(shè)備為超寬屏,scaleRatio 應(yīng)該按照兩者的高度之比決定,即讓設(shè)計稿保持寬高比的情況下放大到與設(shè)備等高,在寬度上可能留有空白,所以還要做個居中布局。

          具體代碼我封裝成了一個 hook:

          // 屏幕適配,src\hooks\useScreenAdapt.ts
          import _ from 'lodash'
          import { onMounted, onUnmounted } from 'vue'
          
          export default function useScreenAdapt(dWidth: number=1920, dHeight: number=1080) {
            // 節(jié)流
            const throttleAdjustZoom=_.throttle(()=> {
              AdjustZoom()
            }, 1000)
          
            onMounted(()=> {
              AdjustZoom()
              // 響應(yīng)式
              window.addEventListener('resize', throttleAdjustZoom)
            })
          
            // 釋放資源
            onUnmounted(()=> {
              window.removeEventListener('resize', throttleAdjustZoom)
            })
          
            function AdjustZoom() {
              // 設(shè)計稿尺寸及寬高比
              const designWidth=dWidth
              const designHeight=dHeight
              const designRatio=designWidth / designHeight // 1.78
          
              // 當(dāng)前屏幕的尺寸及寬高比
              const deviceWidth=document.documentElement.clientWidth
              const devicHeight=document.documentElement.clientHeight
              const deviceRatio=deviceWidth / devicHeight
          
              // 計算縮放比
              let scaleRatio=1
              // 如果當(dāng)前屏幕的寬高比大于設(shè)計稿的,則以高度比作為縮放比
              if (deviceRatio > designRatio) {
                scaleRatio=devicHeight / designHeight
              } else {
                // 否則以寬度比作為縮放比
                scaleRatio=deviceWidth / designWidth
              }
          
              document.body.style.transform=`scale(${scaleRatio}) translateX(-50%)`
            }
          }
          

          最后是給 body 添加了 transform 屬性,為了實現(xiàn)居中效果,還需要給 body 添加上相應(yīng)樣式:

          /* \src\assets\base.css */
          * {
            box-sizing: border-box;
          }
          
          body {
            position: relative;
            margin: 0;
            width: 1920px;
            height: 1080px;
            transform-origin: left top;
            left: 50%;
            background-color: black;
          }
          

          使用 lodash 實現(xiàn)節(jié)流

          為避免改變屏幕尺寸時過于頻繁觸發(fā) AdjustZoom,我借助 lodash 的 throttle 方法做了個節(jié)流,這就需要安裝 lodash:pnpm add lodash。因為用到了 ts,如果直接引入使用 lodash 會遇到如下報錯:

          我們需要引用它的聲明文件,才能獲得對應(yīng)的代碼補全、接口提示等功能。提示里已經(jīng)告訴了我們解決辦法,就是去安裝 @types/lodash:pnpm add -D @types/lodash,之后就能在 ts 文件中正常使用 lodash 了。

          使用 svg

          頁面頭部使用的就是一張 svg,樣式中給 #top 添加絕對定位 position: absolute; ,目的在于開啟一個單獨的渲染層,以減少之后添加動畫造成的回流損耗:

          <template>
            <main class="main-bg">
              <div id="top"></div>
            </main>
          </template>
          <style scoped>
          #top {
            position: absolute;
            width: 100%;
            height: 183px;
            background-size: cover;
            background-image: url(@/assets/imgs/top_bg.svg);
          }
          </style>
          

          作為背景引入的 top_bg.svg 是我使用 Illustrator 繪制后導(dǎo)出的,繪制時注意做好圖層的命名:

          因為圖層的名稱會影響到導(dǎo)出的 svg 文件中元素的 id 名稱。另外導(dǎo)出的 svg 文件中也可能存在一些中文命名或一些不必要的代碼,我們可以自行修改:

          添加動畫及濾鏡

          使用 Illustrator 繪制的都是靜態(tài)圖形,現(xiàn)在我們以其中一個圓球為例,添加上平移的動畫以及高斯模糊的濾鏡:

          <!-- top_bg.svg 部分代碼 -->
          <?xml version="1.0" encoding="UTF-8"?>
          <svg id="top-bg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1920 183">
            <defs>
              <style>
                #circle-1 {
                  opacity: 0;
                  transform: translate(800px, -18px) scale(0.5);
                  animation: circle-1-ani 1.8s ease-out forwards infinite;
                }
                @keyframes circle-1-ani {
                  90%,
                  100% {
                    opacity: 0.95;
                    transform: translate(600px, 80px) scale(1);
                  }
                }
              </style>
              <filter id="blurMe">
                  <feGaussianBlur stdDeviation="2" />
                </filter>
            </defs>
          	<circle id="circle-1" class="cls-1" r="12.96" filter="url(#blurMe)" />
          </svg>
          

          動畫使用 css 定義,可以直接寫在 <defs> 里的 <style> 中。一旦用到 transform,那么圓的坐標(biāo)系就會移動到圓的中心點,所以我將原本 <circle> 中的用于定義圓心坐標(biāo)的 cx 和 cy 屬性刪除了,通過在 #circle-1 中直接使用 transform: translate(800px, -18px); 來定位圓的初始位置:

          濾鏡定義在 <defs> 里的 <filter> 中,使用的是高斯模糊 <feGaussianBlur>, stdDeviation 用于指定鐘形(bell-curve),可以理解為模糊程度。在圓形 <circle> 上通過 filter 屬性,傳入濾鏡的 id 應(yīng)用濾鏡。

          使用 ECharts

          安裝

          首先是安裝 ECharts:pnpm add echarts。在 npm 的倉庫搜索 echarts 可以看到其帶有如下所示的 ts 標(biāo)志:

          說明它的庫文件中已經(jīng)包含了 .d.ts 文件:

          所以不需要像上面使用 lodash 那樣再去額外安裝聲明文件了。

          封裝組件

          接著就可以封裝 echarts 組件了。組件中只需要提供一個展示圖表的 dom 容器 <div>,然后在 onMounted(確保可以獲取到 dom 容器) 中創(chuàng)建一個 ECharts 實例 myChart,最后通過 myChart.setOption(option) 傳入從父組件獲取的圖表實例的配置項以及數(shù)據(jù) option:

          <!-- src\components\BaseEChart.vue -->
          <template>
            <div ref="mainRef" :style="{ width: width, height: height }"></div>
          </template>
          
          <script lang="ts" setup>
            import * as echarts from 'echarts'
            import { onMounted, onUnmounted, ref } from 'vue'
          
            interface IProps {
              width?: string
              height?: string
              chartOption: echarts.EChartsOption
            }
            const props=withDefaults(defineProps<IProps>(), {
              width: '100%',
              height: '100%'
            })
          
            const mainRef=ref(null)
          
            let myChart: echarts.ECharts | null=null
            onMounted(()=> {
              myChart=echarts.init(mainRef.value, 'dark', { renderer: 'svg' })
              const option=props.chartOption
              myChart.setOption(option)
            })
          
            onUnmounted(()=> {
              // 銷毀 echart 實例,釋放資源
              myChart?.dispose()
            })
          </script>
          

          使用示例

          以左上角的“人均消費金額排名”柱狀圖為例,代碼如下:

          <!-- src\views\HomeView.vue -->
          <template>
            <main class="main-bg">
              <div id="left-top">
                <div class="title">人均消費金額排名</div>
                <div class="sub-title">Ranking of per capita consumption amount</div>
                <BaseEChart :chartOption="amountRankOption" />
              </div>
            </main>
          </template>
          
          <script setup lang="ts">
          import BaseEChart from '@/components/BaseEChart.vue'
          import { amountRankOption } from './config/amount-rank-option'
          </script>
          
          <style scoped>
          #left-top {
            position: absolute;
            top: 130px;
            left: 20px;
            width: 500px;
            height: 320px;
          }
          </style>
          

          在頁面引入 BaseEChart 后,傳入定義好的 amountRankOption 即可:

          // 人均消費金額排名柱狀圖配置
          import * as echarts from 'echarts'
          type EChartsOption=echarts.EChartsOption
          
          export const amountRankOption: EChartsOption={
            grid: {
              top: 20,
              bottom: 50,
              left: 40,
              right: 40
            },
            xAxis: {
              axisTick: {
                show: false // 隱藏 x 坐標(biāo)軸刻度
              },
              data: ['思明', '湖里', '集美', '同安', '海滄', '翔安']
            },
            yAxis: {
              axisLabel: {
                show: false // 隱藏 y 坐標(biāo)軸刻度標(biāo)簽
              },
              splitLine: {
                show: false // 隱藏平行于 x 軸的分隔線
              }
            },
            series: [
              {
                type: 'bar',
                data: [5, 20, 36, 10, 10, 20],
                barWidth: 20 // 設(shè)置柱形的寬度
              }
            ]
          }
          

          至于剩下的圖表的實現(xiàn),只是配置不同而已,如有興趣可以去該項目的 git 倉庫查看。

          數(shù)字滾動動畫

          最后添加成交額的數(shù)字滾動動畫,用到了 countup.js,需要先安裝: pnpm add countup.js。

          使用時,直接 new CountUp() 生成 countUp,第 1 個參數(shù)為要添加動畫的 dom 的 id,第 2 個參數(shù)為動畫結(jié)束時顯示的數(shù)字,還可以傳入第 3 個參數(shù) options 實現(xiàn)一些配置,比如設(shè)置前綴,小數(shù)點等。然后通過 countUp.start() 即可實現(xiàn)動畫效果:

          <!-- src\components\Digital.vue -->
          <template>
            <div>
              <span class="t1">成交額</span>
              <span id="amount" class="t2">150</span>
              <span class="t1">億</span>
            </div>
          </template>
          
          <script lang="ts" setup>
            import { CountUp } from 'countup.js'
            import { onMounted } from 'vue'
          
            onMounted(()=> {
              const countUp=new CountUp('amount', 150)
          
              if (!countUp.error) {
                countUp.start()
              } else {
                console.error(countUp.error)
              }
            })
          </script>
          


          原文鏈接:https://juejin.cn/post/7305434729527181322

          )實驗平臺:正點原子開拓者FPGA 開發(fā)板

          2)摘自《開拓者 Nios II開發(fā)指南》關(guān)注官方微信號公眾號,獲取更多資料:正點原子

          3)全套實驗源碼+手冊+視頻下載地址:http://www.openedv.com/docs/index.html

          第十四uC/GUI顯示線/點實驗

          我們在使用 Nios II 的時候會移植 uC/GUI 來制作精美的 UI,所謂 UI 就是 User Interface 的

          縮寫、GUI 就是 Graphical User Interface 的縮寫,即圖形用戶接口。uC/GUI 是 Micrium 公司研

          發(fā)的通用的嵌入式用戶圖像界面軟件,可以給任何使用圖像 LCD 的應(yīng)用程序提供單獨于處理

          器和 LCD 控制器之外的有效的圖形用戶接口,能夠應(yīng)用于單一任務(wù)環(huán)境,也能夠應(yīng)用于多任

          務(wù)環(huán)境中。本章我們將向大家介紹如何在 Qsys 中移植 uC/GUI,并以 RGB 接口的 4.3 寸、

          480*272 分辨率的 LCD 屏幕為例實現(xiàn)基本的打點畫線功能,本章包括以下幾個部分:

          14.1 簡介

          14.2 實驗任務(wù)

          14.3 硬件設(shè)計

          14.4 軟件設(shè)計

          14.5 下載驗證

          簡介

          當(dāng)前主流的小型嵌入式 GUI 主要有:emWin(uC/GUI)、TouchGFX、Embedded Wizard

          GUI、uGFX 和 MicroChip GUI。當(dāng)然,還有其它的 GUI,以上所列的 GUI 基本上都是收費的,

          但由于 ST 公司購買了 emWin 的版權(quán),得到了定制版的 emWin,然后改了名字叫 StemWin,

          所以當(dāng)用戶在 STM32 芯片上使用 emWin 軟件庫時,是不需要向 emWin 或 ST 公司付費的。也

          正因為 STM32 在工商業(yè)的大范圍使用,使得 emWin 的使用場合更廣、學(xué)習(xí)資料也更多。另外

          uC/GUI 和 emWin 還是有區(qū)別的。uC/GUI 的核心代碼并不是 Micrium 公司開發(fā)的,而是 Segger

          公司為 Micrium 公司定制的圖形軟件庫,當(dāng)然也是基于 Segger 公司的 emWin 圖形軟件庫開發(fā)

          的。在以前較早的版本程序中 uC/GUI 的源代碼是開源的(可以在網(wǎng)上能夠找到),但是新版

          本的程序 emWin 和 uC/GUI 只對用戶提供庫文件,是不開源的。這里為了方便對 GUI 感興趣

          的讀者可以查看 uC/GUI 的底層,我們采用開源的 uC/GUI3.90 版本進(jìn)行移植。那么 uC/GUI 有

          什么特點呢?

          μC/GUI 是一種用于嵌入式應(yīng)用的圖形支持軟件,一種用于為任何使用一個圖形 LCD 的

          應(yīng)用提供一個高效率的,與處理器和 LCD 控制器無關(guān)的圖形用戶界面。它適合于單一任務(wù)和

          多任務(wù)環(huán)境,專用的操作系統(tǒng)或者任何商業(yè)的實時操作系統(tǒng)(RTOS)。μC/GUI 以 C 源代碼形

          式提供。它可以適用于任何尺寸的物理和虛擬顯示,任何 LCD 控制器和 CPU。

          μC/GUI 很適合大多數(shù)的使用黑色/白色和彩色 LCD 的應(yīng)用程序。它有一個很好的顏色管

          理器,允許它處理灰階。μC/GUI 也提供一個可擴展的 2D 圖形庫和一個視窗管理器,在使用

          一個最小的 RAM 時能支持顯示窗口。它的架構(gòu)基于模塊化設(shè)計,由不同的模塊中的不同層組

          成,主要包括:液晶驅(qū)動模塊,內(nèi)存設(shè)備模塊,窗口系統(tǒng)模塊,窗口控件模塊,反鋸齒模塊和

          觸摸屏及外圍模塊。其主要特性包括豐富圖形庫,多窗口、多任務(wù)機制,窗口管理及豐富窗口

          控件類(按鈕、檢驗框、單/多行編輯框、列表框、進(jìn)度條、菜單等),多字符集和多字體支持,

          多種常見圖像文件支持,鼠標(biāo)、觸摸屏支持,靈活自由配制等。另外 μC/GUI 可以在嵌入式系

          統(tǒng)上運行也可以裸機運行。

          μC/GUI 對內(nèi)存的需求如下:

          小的系統(tǒng)(沒有視窗管理器)

          ? RAM:100 字節(jié)

          ? 堆棧:500 字節(jié)

          ? ROM:10~25KB(取決于使用的功能)

          大的系統(tǒng)(包括視窗管理器和控件)

          ? RAM:2~6KB(取決于所需窗口的數(shù)量)

          ? 堆棧:1200 字節(jié)

          ? ROM:30~60KB(取決于使用的功能)

          注意,如果應(yīng)用程序使用許多字體的話,ROM 的需求將增加。以上所有的數(shù)值都是粗略

          的估計,根據(jù)實際的應(yīng)用有所區(qū)別。

          另外我們需要說明的一點是對于 μC/GUI 而言屏幕坐標(biāo)如下:


          圖 14.1.1 屏幕坐標(biāo)

          顯示平面由二維坐標(biāo) X 軸和 Y 軸表示,即值(X,Y)。水平刻度被稱作 X 軸,而垂直刻度被

          稱作 Y 軸。在程序中需要用到 X 和 Y 坐標(biāo)時,X 坐標(biāo)總在前面。顯示屏(或者一個窗口)的

          左上角為一默認(rèn)的坐標(biāo)(0,0)。正的 X 值方向總是向右;正的 Y 值方向總是向下。上圖說明

          該坐標(biāo)系和 X 軸和 Y 軸的方向。另外所有傳遞到一個 API 函數(shù)的坐標(biāo)總是以像素(屏幕由能

          夠被單獨控制的許多點組成,這些點被稱作像素)為單位所指定。大部分 μC/GUI 在它的 API

          中向用戶程序提供的文本和繪圖函數(shù)能夠在任何指定像素上寫或繪制。

          實驗任務(wù)

          本章我們首先將 μC/GUI 移植到 Nios II 上運行,然后實現(xiàn)基本的打點畫線功能。

          硬件設(shè)計

          本章實驗工程可基于《lcd_all_Colorbar》實驗上搭建,所以這里我們直接在該實驗工程

          上進(jìn)行移植效果顯示。硬件設(shè)計部分不變,只需修改軟件設(shè)計部分。

          軟件設(shè)計

          我們打開軟件工程后,關(guān)閉原先的工程,新建一個工程,命名為 qsys_gui,然后將原先工

          程的源代碼文件添加進(jìn)來(APP 目錄),并將之前的 main.c 替換現(xiàn)在的 hello_world.c。現(xiàn)在我

          們開始移植 uC/GUI。

          在開始移植 uC/GUI 之前,我們建議大家最好先瀏覽一下《uC/GUI 中文手冊》,該手冊可

          以在網(wǎng)上下載,也可在我們提供的軟件資料中找到,《uC/GUI 中文手冊》里面詳細(xì)的介紹了

          uC/GUI 的所有 API 函數(shù)及相關(guān)例程,并提供了配置說明。通過閱讀《uC/GUI 中文手冊》,我

          們可以進(jìn)一步了解 uC/GUI,加快移植的速度,減少移植的彎路。下面我們就開始進(jìn)行移植。

          第一步:添加需要的功能文件。

          首先我們新建一個名為 uCGUI 的文件夾,用來存放我們需要的 uCGUI 源碼,新建好文件

          夾以后,我們將光盤中的 uCGUI3.90 源碼復(fù)制出來并解壓,解壓完成以后,我們可以看到該源

          碼中有三個文件夾分別為:Sample 文件夾、Start 文件夾和 Tool 文件夾。首先,我們將 Start 文

          件夾中的 Config 文件夾復(fù)制到 uCGUI 中,然后我們將 Start 文件夾下的 GUI 文件夾中的所有

          文件夾都復(fù)制到 uCGUI 中,最后我們再將 Sample 文件夾下的 GUI_X 和 GUIDemo 這兩個文

          件夾復(fù)制到 uCGUI 中,至此我們就完成了移植第一步,最終我們 uCGUI 文件夾中的內(nèi)容,如

          下圖所示。


          圖 14.4.1 uCGUI文件夾中的內(nèi)容

          上圖各個文件夾的內(nèi)容如下:


          圖 14.4.2 文件夾內(nèi)容詳解

          其中 AntiAlias、JPEG、Mendev、Widget、WM 和 GUIDemo 為可選項,前四項可以依據(jù)

          項目的需要而增刪,GUIDemo 是 uC/GUI 自帶的 Demo,如果不需要演示該 Demo,則可以不

          添加。

          第二步:修改相應(yīng)的配置文件。

          首先我們修改 Config 文件夾下的 GUIConf.h 文件,該文件修改后代碼如下所示。

          1 #ifndef GUICONF_H

          2 #define GUICONF_H

          3

          4 #define GUI_OS (0) /* 支持多任務(wù)處理 */

          5 #define GUI_SUPPORT_TOUCH (0) /* 支持觸摸 */

          6 #define GUI_SUPPORT_UNICODE (1) /* 支持 Unicode */

          7

          8 #define GUI_DEFAULT_FONT &GUI_Font6x8 /* GUI 默認(rèn)字體 */

          9 #define GUI_ALLOC_SIZE 12500 /* 動態(tài)內(nèi)存的大小*/

          10 //#define GUI_ALLOC_SIZE 1024*1024

          11

          12 #define GUI_WINSUPPORT 1 /* 支持窗口管理 */

          13 #define GUI_SUPPORT_MEMDEV 1 /* 支持內(nèi)存設(shè)備 */

          14 #define GUI_SUPPORT_AA 1 /* 支持抗鋸齒顯示 */

          15

          16 #endif /* Avoid multiple inclusion */

          該文件是GUI的基本屬性配置文件,它有一些開關(guān)可以配置,比如是否支持系統(tǒng)(GUI_OS),

          是否支持觸摸(GUI_SUPPORT_TOUCH)等。如果我們需要支持系統(tǒng),可將相應(yīng)的值設(shè)為 1,

          如果不需要就設(shè)為 0,此處我們不需要系統(tǒng),所以將其設(shè)置為 0,其余以次類推。動態(tài)內(nèi)存大

          小 GUI_ALLOC_SIZE 可根據(jù)需求設(shè)置,這里我們設(shè)置為 12500,修改好該文件后,我們修改

          Config 文件夾下的 GUITouchConf.h 文件,即 GUI 的觸摸配置文件,因為在 GUIConf.h 文件中

          我們將觸摸的宏設(shè)置為 0,即不使用觸摸功能,所以無需配置該文件,但還是可以看一下該文

          件的內(nèi)容,如下所示。

          1 #ifndef GUITOUCH_CONF_H

          2 #define GUITOUCH_CONF_H

          3

          4

          5 #define GUI_TOUCH_AD_LEFT 3750

          6 #define GUI_TOUCH_AD_RIGHT 300

          7 #define GUI_TOUCH_AD_TOP 420

          8 #define GUI_TOUCH_AD_BOTTOM 3850

          9 #define GUI_TOUCH_SWAP_XY 0

          10 #define GUI_TOUCH_MIRROR_X 0

          11 #define GUI_TOUCH_MIRROR_Y 1

          12

          13 #endif /* GUITOUCH_CONF_H */

          該文件用來配置觸摸屏的一些參數(shù),可根據(jù)實際需求來配置。接下來我們修改 Config 文

          件夾下的 LCDConf.h 文件,該文件修改后代碼如下所示。

          1 #ifndef LCDCONF_H

          2 #define LCDCONF_H

          3

          4 /*********************************************************************

          5 *

          6 * General configuration of LCD

          7 *

          8 **********************************************************************

          9 */

          10

          11 #define LCD_XSIZE (272) /* 配置 TFT 的水平分辨率 */

          12 #define LCD_YSIZE (480) /* 配置 TFT 的垂直分辨率 */

          13

          14 #define LCD_BITSPERPIXEL (16) /* 每個像素的位數(shù) */

          15

          16 #define LCD_CONTROLLER (666) /* TFT 控制器的名稱 */

          17 #define LCD_FIXEDPALETTE (565) /* 調(diào)色板格式 */

          18 #define LCD_SWAP_RB (1) /* 紅藍(lán)反色交換 */

          19 // #define LCD_SWAP_XY (1)

          20 #define LCD_INIT_CONTROLLER() LCD_L0_Init(); /* TFT 初始化函數(shù) */

          21

          22 #endif /* LCDCONF_H */

          該文件用來設(shè)置 TFT LCD 相關(guān)的參數(shù),比如 TFT LCD 的分辨率、像素位數(shù)等,另外還可

          以配置 TFT LCD 的寄存器(若有)和 TFT LCD 初始化入口等,這個文件與硬件直接相關(guān),一

          般是根據(jù)使用的 TFT LCD 來配置。

          第三步:與硬件底層對接

          TFT LCD 的對接:uC/GUI 自帶了很多驅(qū)動,支持很多屏幕,由于我們使用的 4.3 寸 RGB

          TFT LCD 屏幕,uC/GUI 自帶的驅(qū)動中并沒有該屏幕的驅(qū)動,所以這里先將 uCGUI/LCDDriver

          目錄下的文件先全部刪除,然后添加修改好的屏幕驅(qū)動文件 LCD_driver 文件,如下圖所示:


          圖 14.4.3 與硬件底層對接文件

          對該文件我們簡單的介紹下如果需要修改需要注意的事項。首先 TFT 控制器的名稱需要

          對應(yīng)。下圖 53 行的 LCD_CONTROLLER 應(yīng)與我們上面修改的 LCDConf.h 里的#define

          LCD_CONTROLLER 相對應(yīng)。


          圖 14.4.4 修改參數(shù)

          其次對于不同的屏幕需要相應(yīng)修改下面的函數(shù)。


          圖 14.4.5 修改函數(shù)

          另外如果需要觸摸支持的話,還需要觸摸的對接,因為這里我們不使用觸摸,就不做介紹。

          第四步:添加到工程

          現(xiàn)在,我們進(jìn)行最后一步,將 uC/GUI 添加到工程中,實驗一下可行性。添加的方法很簡

          單,將 uCGUI 文件夾復(fù)制到該工程目錄下,即 qsys/software/qsys_gui 文件夾下,然后我們在

          Eclipse 軟件工程中刷新該工程(在左邊的工程欄按快捷鍵 F5,或右鍵點擊應(yīng)用工程文件夾

          qsys_gui 后點擊 fresh),當(dāng)然了,我們也可以直接將我們的 uCGUI 文件夾粘貼至我們 Eclipse

          軟件工程中的結(jié)構(gòu)下。添加完成后,如下圖:

          圖 14.4.6 添加uC/GUI

          此時我們還不能使用 uCGUI,還需要將該文件夾的路徑添加到我們的工程中。添加方法如

          下:我們右鍵點擊應(yīng)用工程文件夾 qsys_gui,在彈出的菜單欄中點擊【Properties】菜單,彈出

          屬性頁面如下圖,點擊 Nios IIApplication Properties 下的 Nios IIApplication Paths,在 Applicatuin

          include directories 欄下點擊 Add…按鈕,將工程下的 uCGUI文件目錄下的子目錄除了 GUIDemo

          外一個個添加進(jìn)來(也可只添加我們需要的功能目錄)。

          圖 14.4.7 添加子目錄

          添加完成后,點擊“OK”按鈕即可。現(xiàn)在我們 ucgui 的移植基本完成。試一下基本的打點

          畫線功能。

          我們修改 qsys_gui.c 的代碼如下:

          1 #include <stdio.h>

          2 #include "system.h"

          3 #include "io.h"

          4 #include "alt_types.h"

          5 #include "altera_avalon_pio_regs.h"

          6 #include "sys/alt_irq.h"

          7 #include "unistd.h"

          8 #include <string.h>

          9 #include "mculcd.h"

          10 #include "GUI.h"

          11

          12 _lcd_gui lcdgui;

          13 extern _lcd_dev lcddev; //管理 LCD 重要參數(shù)

          14

          15 //SDRAM 顯存的地址

          16 alt_u16 *ram =(alt_u16 *)(SDRAM_BASE + SDRAM_SPAN - 2049000);

          17

          18 int main()

          19 {

          20 printf("Hello from NiosII!\n");

          21

          22 MY_LCD_Init(); //LCD 初始化

          23 GUI_Init(); //uC/GUI 初始化

          24

          25 lcdgui.width =lcddev.height;

          26 lcdgui.height =lcddev.width;

          27

          28 GUI_SetBkColor(GUI_VERYLIGHTCYAN); //設(shè)置 GUI 背景色

          29 GUI_Clear(); //GUI 清屏

          30

          31 GUI_SetPenSize(10); //設(shè)置點的大小

          32 GUI_SetColor(GUI_RED); //設(shè)置 GUI 前景色

          33 GUI_DrawPoint(lcdgui.width/2,lcdgui.height/2); //畫點

          34 GUI_DrawLine(0,lcdgui.height/2 + 11,lcdgui.width,lcdgui.height/2 + 11); //畫線

          35

          36 alt_dcache_flush_all();

          37

          38 return 0;

          39 }

          這里我們先設(shè)置了 GUI 背景色,然后清屏,此時清屏?xí)援?dāng)前的背景色清屏。使用的帶

          GUI 的函數(shù)都可從《uC/GUI 中文手冊》中找到,這里我們就不再做詳細(xì)的介紹。該代碼實現(xiàn)的

          功能是在屏幕的正中間畫了一個點,然后在點的下面畫了一條橫線。

          軟件部分就介紹到這里,接下來我們進(jìn)行下載驗證。

          下載驗證

          講完了軟件工程,接下來我們就將該實驗下載至我們的開拓者開發(fā)板進(jìn)行驗證。

          首先我們將 4.3 寸的 ATK-4.3’RGBLCD 與開發(fā)板上的 RGB LCD 接口連接。再將下載器

          一端連電腦,另一端與開發(fā)板上對應(yīng)端口連接,最后連接電源線并打開電源開關(guān)。

          我們在 Quartus II 軟件中將 lcd_all_colorbar.sof 文件下載至我們的開拓者開發(fā)板,下載完

          成后,我們還需要在 Nios II SBT for Eclipse 軟件中將 qsys_gui.elf 文件下載至我們的開拓者開

          發(fā)板,qsys_gui.elf 下載完成以后,我們的 C 程序?qū)?zhí)行在我們的開拓者開發(fā)板上。顯示的

          效果如下圖所示。


          圖 14.5.1 實驗結(jié)果圖

          至此,我們的 uC/GUI 移植和實現(xiàn)打點畫線實驗就完成了。


          主站蜘蛛池模板: 亚洲中文字幕一区精品自拍| 国产精品视频一区二区三区无码| 麻豆国产在线不卡一区二区| 久久精品人妻一区二区三区| 国产91大片精品一区在线观看| 日韩精品无码久久一区二区三| 无码人妻一区二区三区免费| 色欲综合一区二区三区| 亚洲AV永久无码精品一区二区国产| 精品福利视频一区二区三区| 久久精品视频一区| 日韩精品一区二区三区不卡 | 国产在线精品一区在线观看| 亚洲视频一区在线| 日韩精品无码一区二区三区免费 | 人妻无码一区二区三区免费| 中文字幕精品一区| 色狠狠色噜噜Av天堂一区| 久久久国产一区二区三区| 中文字幕在线一区| 亚洲日韩精品无码一区二区三区| 精品一区二区三区四区在线 | 亚洲综合无码一区二区痴汉| 精品国产鲁一鲁一区二区| 亚洲AV无码一区二区三区牲色| 韩国福利影视一区二区三区| 麻豆国产一区二区在线观看| 日本一区二区三区不卡视频| 久久蜜桃精品一区二区三区| 日韩精品国产一区| 精品视频一区二区| 在线观看国产一区| 亚洲一区二区成人| 在线精品亚洲一区二区| 精品亚洲福利一区二区| 一区二区手机视频| 午夜福利av无码一区二区| 精品一区精品二区制服| 午夜DV内射一区区| 好湿好大硬得深一点动态图91精品福利一区二区 | 国产一区二区电影|