MACSS(Sentence Modular And Compound Structure)是一種將 CSS 代碼分解為相互關(guān)聯(lián)的獨(dú)立模塊的方法。它可以提高 CSS 代碼的可重用性、可維護(hù)性和可讀性。
SMACSS 的優(yōu)勢
* 可重用性:使用模塊可以提高 CSS 代碼的可重用性。
* 可維護(hù)性:結(jié)構(gòu)化代碼簡化了維護(hù)。
* 可讀性:清晰的模塊結(jié)構(gòu)易于閱讀和理解。
SMACSS 的核心概念
1. 句法 (Sentence)
* 完整的 CSS 語句。
* 應(yīng)該表示一個(gè)可重用的獨(dú)立功能或外觀。
2. 模塊 (Module)
* 組織 CSS 規(guī)則的組。
* 每個(gè)模塊應(yīng)表示界面設(shè)計(jì)中的特定概念或功能。
3. 組合 (Compound)
* 簡單的 CSS 選擇器,包含多個(gè)元素。
* 應(yīng)用于將多個(gè)元素組合成更復(fù)雜的結(jié)構(gòu)。
工具和集成
* 各種工具可用于使用 SMACSS。
* 許多 IDE 和 CSS 管理工具集成了 SMACSS 功能。
使用 SMACSS 的優(yōu)點(diǎn)
* 提高 CSS 代碼的可重用性。
* 提高 CSS 代碼的可維護(hù)性。
* 提高 CSS 代碼的可讀性。
結(jié)論
SMACSS 是一種強(qiáng)大的 CSS 組織方法,可以提高 CSS 代碼的可重用性、可維護(hù)性和可讀性。它是現(xiàn)代 web 開發(fā)中使用最廣泛的組織方法之一。
在本書的前三部分中,我們一直在應(yīng)用各種 D3 技術(shù)來開發(fā)眾所周知的可視化布局,如條形圖、流圖、直方圖、地圖等。但是,如果您選擇 D3 作為數(shù)據(jù)可視化工具,那么您很有可能還希望構(gòu)建復(fù)雜且不尋常的可視化。若要?jiǎng)?chuàng)建獨(dú)特的項(xiàng)目,需要了解 D3 可以使用的不同方法和布局。與其說是詳細(xì)了解每種方法,不如說是掌握 D3 背后的哲學(xué),并知道在需要時(shí)在哪里查找信息。附錄 C 中,我們映射了所有 D3 模塊及其內(nèi)容,可以為您提供幫助。創(chuàng)建自定義布局所需的另一項(xiàng)技能是將想法和幾何分解為代碼的能力,我們將在本章的項(xiàng)目中執(zhí)行此操作。
該項(xiàng)目將帶您了解創(chuàng)建完全自定義可視化的幕后情況,從草圖創(chuàng)意到將項(xiàng)目分解為組件,再到將視覺元素渲染到徑向布局上。我們將建造的項(xiàng)目探索了文森特梵高在他生命的最后十年中產(chǎn)生的藝術(shù)遺產(chǎn)。您可以在 https://d3js-in-action-third-edition.github.io/van_gogh_work/ 找到已完成的項(xiàng)目。
我們將遵循一個(gè)六步過程來使這個(gè)項(xiàng)目栩栩如生。雖然這不是一成不變的,但這大致是任何數(shù)據(jù)可視化項(xiàng)目都可以遵循的方法。
收集和清理數(shù)據(jù)是任何數(shù)據(jù)可視化項(xiàng)目中最關(guān)鍵的一步。如果幸運(yùn)的話,我們得到了現(xiàn)成的數(shù)據(jù)集,可以直接開始可視化,就像本書以前的項(xiàng)目一樣。但通常情況下,我們需要從不同來源收集數(shù)據(jù),對(duì)其進(jìn)行分析,清理數(shù)據(jù)并對(duì)其進(jìn)行格式化。數(shù)據(jù)收集和操作可能需要大量的時(shí)間。它需要耐心和勤奮。在本節(jié)中,我們將討論為本章項(xiàng)目準(zhǔn)備數(shù)據(jù)所經(jīng)歷的不同步驟。
但在我們尋找數(shù)據(jù)之前,讓我們花點(diǎn)時(shí)間定義我們想要可視化的信息類型。這個(gè)項(xiàng)目的靈感來自Frederica Fragapane的數(shù)據(jù)可視化研討會(huì),在此期間,我們使用文森特梵高寫給他兄弟西奧的信的數(shù)據(jù)集。我們對(duì)梵高的豐富文學(xué)遺產(chǎn)感到震驚,并認(rèn)為將其與他的著名繪畫和素描相結(jié)合以深入了解他的整個(gè)藝術(shù)遺產(chǎn)會(huì)很有趣。
所以,我們知道我們想收集有關(guān)梵高的繪畫、素描和信件的數(shù)據(jù)。理想情況下,我們希望及時(shí)放置這些作品,以可視化他藝術(shù)作品的起伏。經(jīng)過幾次谷歌搜索,我們找到了以下資源:
通過探索這些資源,我們還注意到,我們可以根據(jù)梵高居住的城市將他的生活分解為幾個(gè)階段。例如,他于1886年從荷蘭搬到巴黎,在那里他遇到了保羅·高更和亨利·德·圖盧茲-勞特累克,僅舉兩例。這些藝術(shù)相遇無疑影響了梵高的作品。我們還知道,他從 1889 年 1890 月到 1890 年 <> 月在圣保羅德莫索萊精神病院住院。在此期間,他開始將漩渦融入他對(duì)醫(yī)院花園的描繪中。最后,梵高于 <> 年 <> 月自殺身亡,標(biāo)志著他多產(chǎn)的十年藝術(shù)創(chuàng)作的戛然而止。意識(shí)到這些事件,我們希望我們的可視化構(gòu)成梵高過去十年的時(shí)間線。
現(xiàn)在,我們需要從找到的資源中提取數(shù)據(jù)。讓我們以繪畫為例(https://en.wikipedia.org/wiki/List_of_works_by_Vincent_van_Gogh)。這個(gè)維基百科頁面包含一系列表格,列出了一千多幅畫作。不是我們想要手動(dòng)提取的東西!您可以找到從網(wǎng)頁中提取表并將其轉(zhuǎn)換為 CSV 文件(如 tableconvert.com)的聯(lián)機(jī)服務(wù)。此類工具使用方便快捷。但是如果我們想要更細(xì)粒度的控制,我們可以編寫一個(gè)簡單的腳本來完成這項(xiàng)工作。
14.1 例包含一個(gè)腳本,您可以使用該腳本從維基百科頁面中提取每幅畫的標(biāo)題、圖像 URL 和媒介。要使用此腳本,請(qǐng)打開瀏覽器的控制臺(tái),復(fù)制粘貼整個(gè)代碼段,然后單擊 Enter 。
如果我們看一下頁面結(jié)構(gòu),我們會(huì)發(fā)現(xiàn)它由一系列HTML表格組成,每個(gè)表格都包含使用相同媒介制作的繪畫列表。前六張表是關(guān)于油畫的;第七幅包含水彩畫;第八和第九是關(guān)于石版畫和蝕刻版畫的,我們將將它們歸入“印刷”媒介。最后一個(gè)表格包含字母草圖,我們還不想提取。在示例 14.1 中,我們聲明了一個(gè)數(shù)組,其中包含我們感興趣的表的索引及其相關(guān)介質(zhì)。
然后,我們使用文檔方法querySelectorAll()和類“wikitable”和“sortable”作為選擇器從頁面中提取所有HTML表。我們通過打開瀏覽器檢查器并仔細(xì)查看標(biāo)記來找到這個(gè)選擇器,以找到我們感興趣的表的唯一且通用的選擇器。
在循環(huán)遍歷這些表時(shí),我們檢查它們是否已存在于腳本開頭聲明的 tables 數(shù)組中。這種驗(yàn)證使我們能夠避免從字母草圖表中提取信息。然后,我們可以遍歷每個(gè)表格行并提取繪畫圖像的標(biāo)題和 URL。請(qǐng)注意我們必須如何在代碼中適應(yīng)不同的 DOM 結(jié)構(gòu),因?yàn)楸硇械母袷讲灰恢隆Ec這些HTML結(jié)構(gòu)不匹配的繪畫將被賦予標(biāo)題和圖像URL為null,稍后將手動(dòng)完成。處理現(xiàn)實(shí)生活中的數(shù)據(jù)通常是混亂的!您還將看到我們從 srcset 屬性而不是 src 中提取圖像 URL,因?yàn)榇藞D像更小,并且在我們的項(xiàng)目中需要更少的加載時(shí)間。
最后,我們將繪畫信息構(gòu)建成一個(gè)對(duì)象,并將其推送到一個(gè)名為“繪畫”的數(shù)組中。但是,通過將此數(shù)組記錄到控制臺(tái)中,我們可以將其復(fù)制粘貼到代碼編輯器中并創(chuàng)建一個(gè) JSON 文件。
此腳本針對(duì)此特定示例量身定制,在其他網(wǎng)頁上沒有幫助。但是你可以看到你的JavaScript技能對(duì)于從網(wǎng)頁中提取任何信息是多么有價(jià)值。
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é)太長,我們不會(huì)在這里這樣做,但如果您想練習(xí)從網(wǎng)頁中提取數(shù)據(jù),請(qǐng)嘗試一下!請(qǐng)注意,我們還必須操作提取的數(shù)據(jù)以分別存儲(chǔ)繪畫的寬度和高度,以及創(chuàng)作的月份和年份。需要一些額外的研究來找到一些繪畫的創(chuàng)作月份并找到它們的主題(肖像、靜物、風(fēng)景等)。
如果您想直接跳轉(zhuǎn)到使用數(shù)據(jù),本章的代碼文件包含梵高的繪畫、素描、信件和他所居住城市的時(shí)間軸的現(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)。
在第3章中,我們定義了兩類主要數(shù)據(jù):定量和定性,如圖14.1所示。定量數(shù)據(jù)由數(shù)字信息組成,例如股票市場行為價(jià)值的起伏或教室里的學(xué)生人數(shù)。定量數(shù)據(jù)可以是離散的,由無法細(xì)分的整數(shù)組成,也可以是連續(xù)的,其中數(shù)字在細(xì)分為較小的單位時(shí)仍然有意義。另一方面,定性數(shù)據(jù)是非數(shù)字信息,例如國家列表或星巴克咖啡訂單的可用尺寸(矮、高、大、通風(fēng)等)。定性數(shù)據(jù)可以是名義數(shù)據(jù)(值沒有特定順序)或順序(順序很重要)。
由于我們不會(huì)使用相同的通道來可視化不同的數(shù)據(jù)類型,因此編寫一個(gè)可用于項(xiàng)目的變量列表并按數(shù)據(jù)類型組織它們通常很有幫助。此步驟可以幫助我們識(shí)別可以使用的不同視覺通道或編碼數(shù)據(jù)的方法。圖14.2說明了定量數(shù)據(jù)通常通過位置(如散點(diǎn)圖)、長度(如條形圖)、面積(如我們的羅密歐與朱麗葉項(xiàng)目中節(jié)點(diǎn)的大小)(見第12章)、角度(如餅圖)或連續(xù)色標(biāo)進(jìn)行可視化。另一方面,定性數(shù)據(jù)通常使用分類色階、圖案、符號(hào)、連接(如網(wǎng)絡(luò)圖)或分層數(shù)據(jù)的外殼(如圓形包)進(jìn)行翻譯。這樣的列表只能是不完整的,因?yàn)橹灰幸稽c(diǎn)創(chuàng)造力,我們就可以設(shè)計(jì)出可視化數(shù)據(jù)的新方法。但它提供了我們可以使用的主要視覺編碼的概述。
在這一點(diǎn)上,一個(gè)有用的練習(xí)包括列出數(shù)據(jù)集中包含的不同數(shù)據(jù)屬性,識(shí)別定量和定性數(shù)據(jù),并集思廣益我們希望如何可視化主要屬性。在圖 14.3 中,我們列出了該項(xiàng)目的四個(gè)數(shù)據(jù)集(梵高的畫作列表、他的繪畫列表、他每月寫的信數(shù)量以及他職業(yè)生涯中居住的城市的時(shí)間線),并確定數(shù)據(jù)屬性是定量的(藍(lán)點(diǎn))還是定性的(紅點(diǎn))。基于這些信息,我們可以開始考慮要?jiǎng)?chuàng)建的可視化。
在這個(gè)項(xiàng)目中,我們希望在時(shí)間軸上展示梵高的藝術(shù)作品(繪畫、素描和信件),以探索每種表達(dá)方式的使用與藝術(shù)家在荷蘭和法國的移動(dòng)如何演變之間的相關(guān)性。我們希望更多地關(guān)注繪畫,并允許用戶單獨(dú)探索它們。如果圓圈代表每幅畫,我們可以使用圓圈的顏色來傳達(dá)繪畫的主題(肖像、靜物、風(fēng)景等),使用它們的大小作為作品的尺寸,并用圓圈的邊框突出顯示介質(zhì)(油畫、水彩或印刷品),如圖 14.4 所示。這些圓圈將定位在某種時(shí)間軸上。
每月制作的圖紙和字母數(shù)量可以通過條形圖或面積圖的長度作為次要信息添加。最后,我們知道我們需要一些可點(diǎn)擊的時(shí)間線來選擇和突出梵高在他生命不同時(shí)期的作品。
一旦選擇了視覺通道,我們就可以開始繪制項(xiàng)目的布局。我們已經(jīng)確定每幅畫將由一個(gè)圓圈表示并定位在時(shí)間軸上。水平軸或垂直軸可以工作,盡管它對(duì)于屏幕來說可能太大。一個(gè)有趣的解決方法可能是徑向時(shí)間軸。與其有一個(gè)很難適應(yīng)移動(dòng)屏幕的大圓圈,不如使用小倍數(shù)方法。小型序列圖是一系列可視化效果,使用相同的比例和軸,但表示數(shù)據(jù)的不同方面。通過這種方法,我們可以每年有一個(gè)輪子,允許我們將它們定位到一個(gè)網(wǎng)格中,如圖 14.5 所示。在桌面上,我們將在左側(cè)顯示可點(diǎn)擊的時(shí)間線,在右側(cè)以三列網(wǎng)格形式布置小型序列可視化。在平板電腦上,網(wǎng)格將減少到兩列,而在移動(dòng)設(shè)備上,我們將使用沒有時(shí)間軸功能的單列網(wǎng)格。
每個(gè)小倍數(shù)將可視化一整年,月份沿圓周分布。對(duì)于每個(gè)月,圖紙的數(shù)量將由面積圖和條形長度的字母數(shù)量表示。代表一個(gè)月內(nèi)繪畫的圓圈將聚集在一起,如圖 14.6 所示。
下一步是創(chuàng)建調(diào)色板并選擇字體。我們需要為八種不同的繪畫主題提供一個(gè)分類的調(diào)色板:自畫像、肖像、農(nóng)民生活、室內(nèi)場景、靜物、風(fēng)景、城市景觀建筑等,以及字母和素描的另一種顏色。創(chuàng)建任何調(diào)色板時(shí),請(qǐng)考慮要在項(xiàng)目中安裝的氛圍。例如,在這里,我們想使用一種歡快的調(diào)色板,靈感來自梵高生命中最后幾年的畫作中的色調(diào)。我們通過從繪畫中提取金色并使用 coolors.co 生成匹配的顏色,從圖 14.7 創(chuàng)建了分類調(diào)色板。對(duì)于分類調(diào)色板來說,八種顏色已經(jīng)很多了,因此我們不得不對(duì)某些類別使用類似的色調(diào)。例如,我們?yōu)樾は瘢?c16e70)選擇了舊玫瑰色,為自畫像選擇了相同顏色的較亮版本(#f7a3a6)。您還可以在 adobe.color.com 和 colorhunt.co 上找到調(diào)色板的靈感。
對(duì)于字體,我們發(fā)現(xiàn) font.google.com 是免費(fèi)網(wǎng)絡(luò)字體的絕佳資源。通常,您希望每個(gè)項(xiàng)目最多堅(jiān)持兩個(gè)字體系列,一個(gè)用于標(biāo)題,一個(gè)用于文本正文。一個(gè)簡單的谷歌搜索將為谷歌字體組合提供很多想法。對(duì)于這個(gè)項(xiàng)目,我們選擇了“Libre Baskerville Bold”作為標(biāo)題,一種與19世紀(jì)相呼應(yīng)的襯線字體,文本和標(biāo)簽為“Source Sans Pro”,一種無襯線字體,對(duì)用戶界面具有出色的可讀性。
一旦我們知道我們想要構(gòu)建什么,我們必須決定我們要使用的基礎(chǔ)設(shè)施。因?yàn)檫@個(gè)項(xiàng)目比我們在本書前面創(chuàng)建的項(xiàng)目更復(fù)雜,所以我們將轉(zhuǎn)向JavaScript框架。使用框架將使我們能夠?qū)㈨?xiàng)目分解為小組件,使其更易于開發(fā)和維護(hù)。我們已經(jīng)在第 8 章中使用 React 構(gòu)建了一個(gè)項(xiàng)目,所以這一次,我們將選擇 Svelte,一個(gè)數(shù)據(jù)可視化社區(qū)特別喜歡的簡單編譯器。如果您還不熟悉Svelte,請(qǐng)不要擔(dān)心。本章的重點(diǎn)仍將放在創(chuàng)建復(fù)雜數(shù)據(jù)可視化項(xiàng)目背后的一般原則上。您可以一起閱讀并收集一點(diǎn)點(diǎn)智慧,而不必潛入 Svelte。如果您以前玩過 Svelte 或想嘗試一下,您會(huì)發(fā)現(xiàn)它非常直觀,并且可以很好地與 D3 配合使用。您可以在附錄 E 中找到對(duì) Svelte 的簡要介紹,并在 https://svelte.dev/tutorial 中找到方便的一口大小的教程。
我們希望在將 D3 與 JavaScript 框架或 Svelte 等編譯器相結(jié)合時(shí)將職責(zé)分開。該框架負(fù)責(zé)添加、刪除和操作 DOM 元素,而 D3 用于執(zhí)行與比例、形狀生成器、力布局等可視化相關(guān)的計(jì)算。簡而言之,您需要忘記數(shù)據(jù)綁定模式,并謹(jǐn)慎使用 D3 轉(zhuǎn)換以避免 D3 和 Svelte 之間的沖突。回到第8章,深入討論將D3與框架相結(jié)合的可能方法。
若要開始處理本章的項(xiàng)目,請(qǐng)?jiān)诖a編輯器中打開 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 安裝項(xiàng)目依賴項(xiàng)。然后用 npm run dev 啟動(dòng)項(xiàng)目。該項(xiàng)目將在您的瀏覽器中提供,網(wǎng)址為 http://localhost:5173/ .您將在 src/ 文件夾中找到我們將處理的所有文件。
從第一章開始,我們采用了一種簡單而有效的方法來使SVG圖形響應(yīng):通過設(shè)置SVG容器的viewBox屬性并將寬度和高度屬性留空。這種方法非常容易實(shí)現(xiàn),并且開箱即用。唯一真正的缺點(diǎn)是,當(dāng) SVG 容器變小時(shí),它包含的文本元素會(huì)按比例變小,使它們可能難以閱讀。
在此項(xiàng)目中,我們將采用不同的方法,設(shè)置 SVG 容器的寬度和高度屬性并將 viewBox 留空。每當(dāng)屏幕尺寸發(fā)生變化時(shí),我們將使用事件偵聽器來更新這些屬性。盡管這種方法需要我們作為程序員和瀏覽器付出更多的努力,但它使我們能夠根據(jù)屏幕寬度調(diào)整可視化的布局。此外,隨著屏幕尺寸的減小,它會(huì)保持文本標(biāo)簽的大小。
在之前的項(xiàng)目討論中,我們決定顯示一個(gè)由小型多個(gè)可視化效果組成的網(wǎng)格。所有這些可視化將包含在單個(gè) SVG 元素中。此外,我們將使用一個(gè) 12 列的 flexbox 網(wǎng)格,類似于第 9 章中討論的網(wǎng)格,用于包括時(shí)間軸和可視化效果在內(nèi)的整體頁面布局。
在圖 14.8 中,您可以看到三種不同屏幕寬度的頁面布局:大于或等于 1400px(flexbox 網(wǎng)格容器的寬度)、小于 1400px 但大于 748px 和小于 748px。對(duì)于這三種屏幕尺寸中的每一種,SVG 容器寬度的計(jì)算略有不同。當(dāng)屏幕大于或等于 748px 時(shí),時(shí)間線顯示在左側(cè),從 12 列網(wǎng)格中取出兩列,可視化效果或 SVG 容器顯示在右側(cè)剩余的十列上。當(dāng)屏幕小于 748px 時(shí),我們會(huì)刪除時(shí)間線,可視化效果可以擴(kuò)展到 12 列。彈性框網(wǎng)格容器還應(yīng)用了 30px 的填充到左側(cè)和右側(cè)。
彈性框網(wǎng)格在其 CSS 屬性中的最大寬度限制為 1400 像素。這意味著即使在較大的屏幕上,內(nèi)容也不會(huì)超過此寬度。要計(jì)算寬度超過 1400px 的屏幕上 SVG 容器的寬度,我們可以減去 flexbox 網(wǎng)格容器兩側(cè)的填充,將其乘以 12,然后除以 <>。這是因?yàn)?SVG 容器跨越十二列中的十列。
svg寬度 = 10/12 * (網(wǎng)格容器 - 2*填充)
當(dāng)屏幕小于 1400 像素時(shí),SVG 容器的大小會(huì)按比例變小。在 svgWidth 的方程中,我們只需要更改窗口寬度的 gridContainer。
svg寬度 = 10/12 * (窗口寬度 - 2*填充)
最后,當(dāng)屏幕小于 768px 時(shí),SVG 容器將分布在屏幕的整個(gè)寬度減去填充。
svg寬度 = 窗口寬度 - 2*填充
在示例 14.2 中,我們使用這些方程來動(dòng)態(tài)計(jì)算 SVG 容器的寬度。為此,我們在文件Grid.svelte中工作。我們首先聲明兩個(gè)變量,一個(gè)用于 windowWidth,一個(gè)用于 SVG width。使用 switch 語句,我們根據(jù)屏幕的寬度和剛才討論的方程設(shè)置 SVG width 變量的值。因?yàn)閟witch語句是用Svelte反應(yīng)符號(hào)($)聲明的,所以只要變量包含更改,它就會(huì)運(yùn)行。
請(qǐng)注意我們?nèi)绾螌?windowWidth 變量綁定到 window 對(duì)象的 innerWidth 屬性。在 Svelte 中,我們可以從任何組件訪問窗口對(duì)象 <svelte:window /> .
最后,我們使用 svgWidth 變量動(dòng)態(tài)設(shè)置 SVG 容器的 width 屬性。因?yàn)槲覀兪褂玫氖?JavaScript 框架,所以我們不使用 D3 將 SVG 元素附加到 DOM 中,而是直接將其添加到組件的標(biāo)記中。
<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 元素將包含一個(gè)由多個(gè)小型可視化組成的網(wǎng)格,因此如果我們知道:可視化的數(shù)量、它們的高度和網(wǎng)格中的列數(shù),我們可以計(jì)算它的高度。在示例 14.3 中,我們首先聲明了我們想要可視化梵高工作的年份數(shù)組。我們使用 D3 的范圍方法做到這一點(diǎn)。然后,我們根據(jù)屏幕的寬度設(shè)置網(wǎng)格的列數(shù)。如果屏幕大于 900px,我們需要三列,如果小于 600px,我們需要一列,在兩者之間,我們需要兩列。我們現(xiàn)在使用大概的數(shù)字,如果需要,我們會(huì)在以后進(jìn)行調(diào)整。
一旦我們知道了列數(shù),我們就可以通過將小型多個(gè)可視化的數(shù)量除以列數(shù)并將結(jié)果四舍五入來計(jì)算行數(shù)。通過將 SVG 元素的寬度除以列數(shù)來找到每個(gè)小序列圖的寬度。我們還任意將它們的高度設(shè)置為寬度加 40px。最后,我們通過將行數(shù)乘以每個(gè)小倍數(shù)的高度來找到 SVG 元素的總高度。
由于 svgWidth 和 svgHeight 變量在組件掛載時(shí)為 null,因此瀏覽器將引發(fā)錯(cuò)誤。這就是為什么我們僅在定義了這兩個(gè)變量后才使用條件語句將 SVG 元素添加到標(biāo)記中。請(qǐng)注意 switch 語句和維度變量如何使用 $ 符號(hào)進(jìn)行響應(yīng)。每次屏幕寬度更改時(shí),它們都會(huì)更新。
我們有一個(gè)響應(yīng)式 SVG 元素!這個(gè)實(shí)現(xiàn)需要比我們以前的策略更多的工作,但在下一節(jié)中使用響應(yīng)式 SVG 網(wǎng)格時(shí)會(huì)很有幫助。
<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>
在最后一個(gè)列表中,我們使用變量 smWidth 和 smHeight 確定每個(gè)網(wǎng)格項(xiàng)的寬度和高度。使用這些值,我們將構(gòu)建將保存所有可視化效果的網(wǎng)格。由于我們在 SVG 容器中工作,因此我們將使用組元素來包圍每個(gè)小倍數(shù)。
首先,在清單 14.4 中,我們在 SVG 容器中插入一個(gè) each 塊,用于遍歷先前創(chuàng)建的 years 數(shù)組。值得注意的是,我們可以訪問每年的索引(i)作為第二個(gè)參數(shù)。我們每年創(chuàng)建一個(gè)組元素,然后使用 transform 屬性應(yīng)用翻譯。為了確定每個(gè)組屬于哪一列,我們使用索引的余數(shù),也稱為模數(shù)(% ),除以列數(shù)。下面的等式說明了三列布局中介于 <> 和 <> 之間的索引的余數(shù)。然后,我們通過將余數(shù)乘以 smWidth 來計(jì)算水平平移。
0 % 3 = 0
1 % 3 = 1
2 % 3 = 2
3 % 3 = 0
4 % 3 = 1
5 % 3 = 2
等等...
對(duì)于垂直平移,我們將索引四舍五入除以列數(shù),以了解我們在哪一行,然后將結(jié)果乘以網(wǎng)格元素的高度。然后,我們在組中附加一個(gè)矩形元素,將其尺寸設(shè)置為網(wǎng)格項(xiàng)的寬度和高度,并為其提供藍(lán)色筆觸。我們添加此矩形以確保網(wǎng)格按預(yù)期工作,并在屏幕寬度更改時(shí)正確調(diào)整大小,但我們不會(huì)將其保留在最終可視化效果中。
{#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>
實(shí)現(xiàn)網(wǎng)格后,調(diào)整屏幕大小以確保網(wǎng)格項(xiàng)的列數(shù)和位置按預(yù)期調(diào)整。當(dāng)屏幕大于 900px 時(shí),網(wǎng)格應(yīng)有三列,600 到 900px 之間應(yīng)有兩列,如果小于 600px,則有一列,如圖 14.9 所示。
響應(yīng)式 SVG 網(wǎng)格
準(zhǔn)備好項(xiàng)目骨架后,我們可以開始利用 D3 來創(chuàng)建梵高作品的可視化!在本節(jié)中,我們將構(gòu)建我們的小的多重可視化,從軸和標(biāo)簽開始,繼續(xù)繪畫,最后是繪圖和字母。
我們的小型多重可視化的主干可以簡化為背景圓圈和年份標(biāo)簽。但在實(shí)施這些元素之前,我們需要定義它們的確切位置。圖 14.10 顯示了在定位圓圈和年份標(biāo)簽之前需要考慮的不同參數(shù)的草圖。我們已經(jīng)計(jì)算了每個(gè)小倍數(shù)的寬度(smWidth)和高度(smHeight)。為了確保可視化之間有足夠的空間并為月份標(biāo)簽留出空間,我們可以定義要在每個(gè)圓圈周圍應(yīng)用的填充,比如說 60px。根據(jù)這個(gè)值和網(wǎng)格元素的寬度,我們可以計(jì)算背景圓的半徑。
我們將開始在 Grid.svelte 的子組件中構(gòu)建可視化,名為 GridItem.svelte 。在清單 14.5 中,我們首先將此組件導(dǎo)入到 Grid.svelte 中。然后,我們將 GridItem 附加到每個(gè)塊中,這將從年份數(shù)組中生成每年的 GridItem。我們將 smWidth 、smHeight 和當(dāng)前年份作為道具傳遞給這個(gè)子組件。
<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 計(jì)算圓的半徑。因?yàn)榘霃奖宦暶鳛橐粋€(gè)反應(yīng)變量 ($),所以只要 smWidth 發(fā)生變化,它就會(huì)被重新計(jì)算。
在標(biāo)記中,我們使用兩個(gè)組元素來設(shè)置可視化的相對(duì)坐標(biāo)系的原點(diǎn)。第一個(gè)水平轉(zhuǎn)換為半 smWidth .它用作年份標(biāo)簽的參考點(diǎn),然后只需將其垂直平移到網(wǎng)格項(xiàng)的底部。第二組元素垂直平移到背景圓的中心。當(dāng)我們開始向可視化追加其他形狀以表示繪畫、素描和字母時(shí),此策略將特別方便。
<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>
下一步是為每個(gè)月添加一個(gè)軸和標(biāo)簽,如圖 14.10 所示。此圖顯示圓坐標(biāo)系的原點(diǎn)位于其中心,這要?dú)w功于之前翻譯的 SVG 組。每個(gè)月的軸將是一條從原點(diǎn)開始并到達(dá)圓周的線,每個(gè)月的角度都不同。
為了計(jì)算軸端點(diǎn)的位置,我們需要做一些三角函數(shù)。讓我們以二月的軸為例。在圖 14.11 的右側(cè),您可以看到我們可以通過將軸與其水平 (x) 和垂直 (y) 邊長連接起來來形成一個(gè)直角三角形(其中一個(gè)角為 90° 角的三角形)。我們也可以稱θ(θ)為12點(diǎn)鐘位置(零度時(shí))與二月軸之間的角度。
三角函數(shù)告訴我們,θ 的正弦等于 x 除以二月軸的長度或背景圓的半徑。因此,我們可以通過將半徑乘以sinθ來計(jì)算端點(diǎn)的水平位置。類似地,θ 的余弦等于 y 除以二月軸的長度。因此,我們可以通過將半徑乘以 cosθ 和 -1 來計(jì)算端點(diǎn)的垂直位置,因?yàn)槲覀冋怪陛S的負(fù)方向前進(jìn)。
sinθ = x / 半徑 => x = 半徑 * sinθ
余量θ = y / 半徑 => y = 半徑 * 余量θ
為了繪制月份軸,我們繼續(xù)在 網(wǎng)格項(xiàng)目.svelte .我們首先聲明一個(gè)點(diǎn)刻度,該刻度將月份數(shù)組作為輸入(該數(shù)組在文件 /utils/months.js 中可用)并返回相應(yīng)的角度。我們希望 12 月顯示在 360 點(diǎn)鐘位置,對(duì)應(yīng)于零角度。我們知道,一個(gè)完整的圓覆蓋 2° 或 2π 弧度。因?yàn)橐荒暧惺€(gè)月,所以我們將刻度中的最后一個(gè)角度設(shè)置為 2π - 12π/<> 弧度,或一個(gè)完整的圓減去十二分之一的圓。
在標(biāo)記中,我們使用每個(gè)塊為每個(gè)月附加一個(gè)行元素。每條線的起點(diǎn)是 (0, 0) ,而它的端點(diǎn)是用剛才討論的三角函數(shù)計(jì)算的。
<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>
作為最后一步,我們要在每個(gè)月的軸上添加一個(gè)標(biāo)簽,在圓圈外 30px。在示例 14.8 中,我們?yōu)槊總€(gè)月附加一個(gè)文本元素,并使用 JavaScript slice() 方法將文本設(shè)置為該月的前三個(gè)字母。為了正確定位文本標(biāo)簽,我們執(zhí)行翻譯,然后旋轉(zhuǎn)。我們發(fā)現(xiàn)帶有三角函數(shù)的平移,類似于我們計(jì)算軸端點(diǎn)的方式。對(duì)于圓圈上半部分(9 點(diǎn)鐘和 3 點(diǎn)鐘之間)顯示的標(biāo)簽,旋轉(zhuǎn)與其軸相同。對(duì)于下半部分(3點(diǎn)鐘和9點(diǎn)鐘之間)的標(biāo)簽,我們給它們額外的180°旋轉(zhuǎn),以便它們更容易閱讀。
雖然我們在 JavaScript Math.sin() 和 Math.cos() 函數(shù)中使用弧度,但旋轉(zhuǎn)的轉(zhuǎn)換屬性需要度數(shù)。為了便于從弧度到度數(shù)的轉(zhuǎn)換,我們創(chuàng)建了一個(gè)名為 radiansToDegrees() 的輔助函數(shù),您可以在 /utils/helper.js 中找到該函數(shù)。它采用弧度的角度作為輸入,并返回相同的弧度角度。
<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>
準(zhǔn)備好軸后,我們可以開始繪制可視化效果了。我們將首先用一個(gè)圓圈來表示梵高的每幅畫。這些圓圈將圍繞相應(yīng)月份軸的端點(diǎn)按年份分組,然后按創(chuàng)建月份分組。如第14.3節(jié)所述,圓圈的顏色將基于繪畫的主題及其在媒介上的邊框。最后,我們將圓的面積設(shè)置為與相關(guān)圖稿的尺寸成比例。圖 14.12 顯示了我們所追求的效果。為了在避免重疊的同時(shí)生成圓簇,我們將使用 D3 的力布局。
在進(jìn)一步進(jìn)入代碼之前,讓我們花點(diǎn)時(shí)間思考一下組件的體系結(jié)構(gòu),并制定最佳前進(jìn)方向的戰(zhàn)略。我們的小倍數(shù)可視化由三層組件組成,如圖 14.13 所示。第一個(gè)由 Grid.svelte 組件持有,該組件負(fù)責(zé)將 SVG 容器添加到標(biāo)記中,并將年份分解為類似網(wǎng)格的布局。該組件“知道”我們將生成可視化的所有年份。
第二層由 GridItem.svelte 處理。此組件僅“感知”單個(gè)年份的數(shù)據(jù),并顯示其相應(yīng)的年份標(biāo)簽和月份軸。最后,還有組件 繪畫.svelte , 圖紙.svelte 和 字母.svelte 。我們還沒有處理這些文件,但它們包含在 chart_components/ 文件夾中。顧名思義,這些組件負(fù)責(zé)可視化一年中產(chǎn)生的繪畫、素描和信件。因?yàn)樗鼈儗腉ridItem.svelte調(diào)用,所以它們也將知道一年的數(shù)據(jù)。
考慮到這種分層架構(gòu),我們看到加載整個(gè)繪畫數(shù)據(jù)集的最佳位置是 Grid.svelte ,因?yàn)樵摻M件監(jiān)督可視化的整體性,并且在應(yīng)用程序中僅加載一次。然后,該組件會(huì)將每年對(duì)應(yīng)的繪畫作為道具傳遞給GridItem.svelte,然后將它們傳遞給Paintings.svelte。
基于這個(gè)邏輯,在清單 14.9 中,我們回到 Grid.svelte 并導(dǎo)入繪畫數(shù)據(jù)集。由于我們稍后希望根據(jù)繪畫的尺寸調(diào)整代表繪畫的圓圈的大小,因此我們計(jì)算這些作品的面積,并使用此信息查找數(shù)據(jù)集中最大的繪畫尺寸。請(qǐng)注意,數(shù)據(jù)集中有一些繪畫的尺寸不可用。在這種情況下,我們將相應(yīng)圓的半徑設(shè)置為 3px。
要將繪畫的面積(以cm 2為單位)縮放為屏幕上的圓形區(qū)域(以px2為單位),我們可以使用線性比例。我們稱此比例為繪畫AreaScale,并使用圓的面積公式找到范圍覆蓋的最大面積:
a = πr2
最后,我們將顯示繪畫所需的數(shù)據(jù)和函數(shù)傳遞給 GridItem .請(qǐng)注意我們?nèi)绾芜^濾繪畫數(shù)據(jù)集以僅傳遞與當(dāng)前年份對(duì)應(yīng)的繪畫。
<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 所示。
<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>
最后,真正的動(dòng)作發(fā)生在《畫畫》中。現(xiàn)在,我們循環(huán)瀏覽作為道具收到的畫作,并在每幅畫的標(biāo)記中添加一個(gè)圓圈。這些圓的初始位置是它們相關(guān)月份軸的尖端,這可以通過我們之前使用的三角函數(shù)找到。我們還必須考慮我們不知道它們是在哪個(gè)月創(chuàng)作的畫作。我們將它們放置在可視化效果的中心。
為了計(jì)算圓的半徑,我們稱之為 繪畫面積比例 .由于此刻度返回一個(gè)面積,因此我們需要使用以下公式計(jì)算相應(yīng)的半徑:
r = √(a/π)
<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}
在這個(gè)階段,繪畫的圓圈在其月軸的頂端重疊,如圖 14.14 所示。我們將在一分鐘內(nèi)通過 D3 的力布局解決這個(gè)問題。
為了在每個(gè)月軸的尖端創(chuàng)建節(jié)點(diǎn)集群,我們將使用 D3 的力布局。這種布局有點(diǎn)復(fù)雜,所以如果你需要更深入的介紹,我們建議閱讀第12章。在示例 14.12 中,我們使用 forceSimulation() 方法初始化一個(gè)新的力模擬,我們將繪畫數(shù)組傳遞給該方法。我們還聲明了一個(gè)空節(jié)點(diǎn)數(shù)組,在每次報(bào)價(jià)后,我們使用模擬的節(jié)點(diǎn)進(jìn)行更新。然后,我們遍歷此節(jié)點(diǎn)數(shù)組而不是繪畫,以將圓圈附加到標(biāo)記中。
我們計(jì)算施加在反應(yīng)塊($ )內(nèi)節(jié)點(diǎn)的力,以便在相關(guān)變量發(fā)生變化時(shí)觸發(fā)重新計(jì)算。在這個(gè)塊內(nèi),定位力(forceX和forceY)將節(jié)點(diǎn)推向其月軸的尖端,而碰撞力(forceCollide)確保節(jié)點(diǎn)之間沒有重疊。
我們還降低了 alpha(模擬的“溫度”)并提高了 alpha 衰減率,以幫助模擬更快地收斂。這種調(diào)整需要反復(fù)試驗(yàn)的方法才能找到正確的設(shè)置。
最后,我們使用仿真添加到節(jié)點(diǎn)的 x 和 y 屬性來設(shè)置相應(yīng)圓的 cx 和 cy 屬性。
<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)該會(huì)看到節(jié)點(diǎn)群集出現(xiàn)在月份軸的提示處。為了完成繪畫可視化,我們將根據(jù)圓圈相應(yīng)繪畫的主題設(shè)置圓圈的顏色。該文件實(shí)用程序/主題.js包含可用的繪畫主題及其顏色的數(shù)組。在示例 14.13 中,我們聲明了一個(gè)序數(shù)尺度,它將主題作為輸入并返回相應(yīng)的圓圈。然后,我們所要做的就是通過調(diào)用此刻度來設(shè)置圓圈的填充屬性。
<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)完成了對(duì)繪畫的可視化!此時(shí),您的可視化效果將類似于圖 14.15 中的可視化效果。
我們的下一步是繪制一個(gè)面積圖,可視化梵高每年完成的繪畫數(shù)量。在第 4 章中,我們學(xué)習(xí)了如何使用 D3 的形狀生成器來計(jì)算折線圖和面積圖路徑元素的 d 屬性。在這里,我們將使用與形狀生成器 lineRadial() 類似的策略,該策略在 d3 形狀模塊中可用。
與上一節(jié)一樣,我們希望考慮用于渲染可視化的三層 Svelte 組件。我們將在 Grid.svelte 中加載整個(gè)圖紙數(shù)據(jù)集,并計(jì)算一個(gè)月的最大圖紙數(shù)量。我們還將重新組織數(shù)據(jù)集以每年拆分信息,如清單 14.14 所示。我們將此信息傳遞給 GridItem.svelte,并初始化一個(gè)刻度,負(fù)責(zé)計(jì)算與許多繪圖對(duì)應(yīng)的沿月軸的徑向位置(參見示例 14.15),并將所有這些信息傳遞給 Drawings.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}
<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() 方法來初始化一個(gè)線生成器。如第 4 章所述,我們設(shè)置其訪問器函數(shù)來計(jì)算每個(gè)數(shù)據(jù)點(diǎn)的位置。但是這一次,我們使用的是極坐標(biāo)而不是笛卡爾坐標(biāo),因此有必要使用 angle() 和 radius() 函數(shù)。當(dāng)我們將 path 元素附加到標(biāo)記時(shí),我們調(diào)用行生成器來設(shè)置其 d 屬性。在樣式中,我們給它一個(gè)半透明的填充屬性。
<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年的面積圖。
我們將可視化梵高作品的最后一部分是他每個(gè)月寫的信的數(shù)量。因?yàn)槟F(xiàn)在擁有所有必需的知識(shí),所以請(qǐng)自己試一試!
您可以自己完成此項(xiàng)目,也可以按照以下說明進(jìn)行操作:
1. 在 Grid.svelte 中加載字母數(shù)據(jù)集。此數(shù)據(jù)集包含每月寫的信件總數(shù)。
2. 通過道具將當(dāng)前年份對(duì)應(yīng)的字母傳遞給 GidItem.svelte。
3. 在 GidItem.svelte 中,導(dǎo)入字母組件。將其添加到標(biāo)記中,并將字母數(shù)據(jù)和比例作為道具傳遞。
4. 在 Letters.svelte 中,為每個(gè)月附加一行,然后根據(jù)相關(guān)字母的數(shù)量并使用三角函數(shù)設(shè)置行的端點(diǎn)。
如果您在任何時(shí)候遇到困難或想將您的解決方案與我們的解決方案進(jìn)行比較,您可以在附錄 D 的 D.14 節(jié)和本章代碼文件的文件夾 14.5.5-Radial_bar_chart / 末尾找到它。但是,像往常一樣,我們鼓勵(lì)您嘗試自己完成它。您的解決方案可能與我們的略有不同,沒關(guān)系!
為了完成可視化的靜態(tài)版本,我們注釋掉之前用于查看網(wǎng)格布局的矩形和圓形并添加時(shí)間線。由于時(shí)間軸與 D3 沒有太大關(guān)系,因此我們不會(huì)解釋代碼,但您可以在附錄 D 的清單 D.14.4 和本章代碼文件的文件夾 14.5.4 中找到它。您也可以將其視為自己構(gòu)建的挑戰(zhàn)!帶有時(shí)間軸的完整靜態(tài)布局如圖 14.7 所示。
現(xiàn)在我們的靜態(tài)項(xiàng)目已經(jīng)準(zhǔn)備就緒,必須退后一步,考慮未來的用戶可能想要如何探索它。他們將尋找哪些其他信息?他們會(huì)問哪些問題?我們可以通過互動(dòng)來回答這些問題嗎?以下是用戶可能會(huì)提出的三個(gè)問題示例:
我們可以通過簡單的交互來回答這些問題:前兩個(gè)帶有工具提示,最后一個(gè)帶有交叉突出顯示。由于本章已經(jīng)很長了,并且此類交互與 D3 無關(guān)(在框架中,我們傾向于避免使用 D3 的事件偵聽器,因?yàn)槲覀儾幌M?D3 與 DOM 交互),因此我們不會(huì)詳細(xì)介紹如何實(shí)現(xiàn)它們。本節(jié)的主要重點(diǎn)是為您提供一個(gè)示例,說明如何規(guī)劃對(duì)項(xiàng)目有意義的交互。您可以在在線托管項(xiàng)目 (https://d3js-in-action-third-edition.github.io/van_gogh_work/) 上使用這些交互,并在本章代碼文件的文件夾 14.6 中找到代碼。下圖也顯示了它們的實(shí)際效果。
我們的項(xiàng)目到此結(jié)束!我們希望它能激發(fā)您對(duì)可視化的創(chuàng)意。如果您想更深入地了解將 D3 與 Svelte 相結(jié)合以實(shí)現(xiàn)交互式數(shù)據(jù)可視化,我們強(qiáng)烈推薦 Connor Rothschild 的 Svelte 更好的數(shù)據(jù)可視化課程 (https://www.newline.co/courses/better-data-visualizations-with-svelte)。
lamp函數(shù)是CSS函數(shù) min() max()的完美結(jié)合。
在研究CSS clamp()之前,我們先看一下這兩個(gè)函數(shù)CSS min() 和 CSS max()。 理解它們將有助于讓 CSS clamp() 更容易理解。
CSS min() 和 CSS max()
CSS min() 允許您將逗號(hào)分隔表達(dá)式列表中的最小值設(shè)置為 CSS 屬性值。CSS max() 將從逗號(hào)分隔的表達(dá)式列表中設(shè)置最大的值作為 CSS 屬性值。
min() 和 max() 都可以在允許 <length>、<frequency>、<angle>、<time>、<percentage>、<number> 或 <integer> 的任何地方使用。
舉個(gè)例子:
width: min(50vw, 700px);
這意味著div的寬度最大為 700 像素,但是,如果 div 在視口寬度的 50% 處更小(50vw = 50 視圖寬度),它將采用兩個(gè)值中的較小值。因此,如果視口為 1300px 寬,則 <div> 最終將是 650px 寬(轉(zhuǎn)換為 50vw),但是,如果視口是 1600px 寬,則 <div> 將只有 700px 寬,因?yàn)槟鞘莾蓚€(gè)可用選項(xiàng)之間的較小值。max的例子也一樣,只不過是取不同情況下的最大值。
雖然 CSS min() 和 CSS max() 在使響應(yīng)式 CSS 設(shè)計(jì)更容易方面取得了長足的進(jìn)步,但開發(fā)人員想要更多,我們希望能夠在同一個(gè) CSS 函數(shù)中定義下限和上限。 于是,CSS clamp() 誕生了。
CSS Clamp()
CSS clamp() 結(jié)合了 CSS min() 和 CSS max() 的優(yōu)點(diǎn)。 CSS clamp() 本質(zhì)上是在上限和下限之間設(shè)置一個(gè)值。 clamp() 允許在定義的最小值和最大值之間的值范圍內(nèi)選擇中間值。它采用三個(gè)參數(shù):最小值、首選值和最大值。
如果將 clamp() 分解為 CSS min() 和 CSS max() 函數(shù),它會(huì)是這樣的:clamp(MIN, VAL, MAX) 解析為 max(MIN, min(VAL, MAX))。這就是開發(fā)人員過去必須做的事情才能在 CSS 中進(jìn)行擴(kuò)展。
雖然它很容易解釋,但 MIN 值是最小值。這是允許值范圍內(nèi)的下限。如果首選值小于此值,將使用 MIN 值。
首選 VAL 是表達(dá)式,只要結(jié)果介于 MIN 和 MAX 值之間,就會(huì)使用其值。
MAX 值是最大(最正)表達(dá)式值,如果首選值大于此上限,則使用該值。
比如下面的代碼,當(dāng)75%的值小于350px,就使用350px,當(dāng)75%的值大于800px,就使用800px,介于之間就使用75%。
width: clamp(350px, 75%, 800px);
字體的例子如下:
font-size: clamp(1.5rem, 5vw, 4rem);
總結(jié):
如果您需要讓文本在移動(dòng)設(shè)備上變得更小,但又不是難以閱讀的小字體,clamp() 可以處理。當(dāng)用戶在大型桌面顯示器上時(shí),希望放大該圖像, clamp() 也同樣適用。從現(xiàn)在開始,每當(dāng)我需要在上限和下限內(nèi)響應(yīng)時(shí), 都可以使用CSS clamp()來嘗試。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。