ighcharts 4.1.7發布,新版本新增useHTML選項,對舊版本存在的問題進行了修復。
本站文章除注明轉載外,均為本站原創或翻譯
度地圖開放平臺功能強大,使用簡單,為地圖的自定義提供了非常方便的途徑!
本文以繪制一張全國機器輻射圖為例記錄其基本使用方法,效果如下圖:
圖中包括了帶圖標和文本的標注,連線以及圖例。
1.關于坐標
說到地圖,不得不說坐標。
我以為,GPS獲取經緯度之后,把經緯度丟給地圖就可以了。但那真的是自以為。
1.1 坐標系
來看看實際情況,以下是百度開發文檔里的描述:
目前國內主要有以下三種坐標系:
WGS84:為一種大地坐標系,也是目前廣泛使用的GPS全球衛星定位系統使用的坐標系。
GCJ02:又稱火星坐標系,是由中國國家測繪局制訂的地理信息系統的坐標系統。由WGS84坐標系經加密后的坐標系。
BD09:為百度坐標系,在GCJ02坐標系基礎上再次加密。其中bd09ll表示百度經緯度坐標,bd09mc表示百度墨卡托米制坐標。
非中國地區地圖,服務坐標統一使用WGS84坐標。
百度對外接口的坐標系為BD09坐標系,并不是GPS采集的真實經緯度,在使用百度地圖JavaScript API服務前,需先將非百度坐標通過坐標轉換接口轉換成百度坐標。
通過 GPS 獲取的為 WGS84,在百度地圖上使用前要轉換為 BD09,百度提供了相應的 api 進行坐標轉換,文檔地址:http://lbsyun.baidu.com/index.php?title=webapi/guide/changeposition
http://api.map.baidu.com/geoconv/v1/?coords=114.21892734521,29.575429778924&from=1&to=5&ak=s1eeiQEfDF0WZfdfvLgHbG2Ru49UNCrn 返回結果: { status : 0, result : [ { x : 114.23074871003, y : 29.579084787993 } ] }
具體還可參考下這篇文章:https://www.cnblogs.com/yesicoo/p/4668642.html
1.2 坐標拾取器
如果坐標是靜態的,或測試用,可以直接通過百度地圖提供的“坐標拾取器”工具來獲取經緯度。
工具地址:http://api.map.baidu.com/lbsapi/getpoint/index.html
點哪就獲取哪的坐標,此坐標不用再轉換,復制過來即可以使用。
2. 開始應用
2.1 準備圖標
有好些站點可以下載圖標,如:https://easyicon.net,可以獲取一些圖標文件。至于商用的要求則要看看站點說明。
如下圖,這里準備總部與機器的圖標下載保存為 head.png、machine.png。
2.2 開啟百度地圖
地圖API的使用需要先申請一個 ak,為了體驗方便,這里已經申請了一個可以直接使用的 key,在頁面中可直接加入以下引用。
<script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=s1eeiQEfDF0WZfdfvLgHbG2Ru49UNCrn"></script>
使用以下語句,定義全局的地圖對象
// 百度地圖 API 功能對象 var map=null; if (BMap) { map=new BMap.Map("allmap"); // id=allmap 的容器內顯示 map.enableScrollWheelZoom(); }
2.2 標注:圖標與文本
標注使用 BMap.Marker,可以為其指定 Icon與Label。為了方便后續使用,本例定義以下函數,指定位置、圖標(本例中可用已經下載的圖標 head,machine)以及文本即可。
/** * 指定經緯度,圖標,標注文本 * 在地圖上添加標注 * longitude 經度 * latitude 緯度 * icon 圖標 * text 標注文本 **/ function addMarker(longitude, latitude, icon, text) { if (!map) return; var point=new BMap.Point(longitude, latitude); var myIcon=new BMap.Icon(icon + ".png", new BMap.Size(32, 32)); // 指定位置及標注的圖標 var marker=new BMap.Marker(point, { icon: myIcon }); // 創建標注 if(text){ var label=new BMap.Label(text, { offset: new BMap.Size(32, -16) }); marker.setLabel(label); } // 添加到地圖上 map.addOverlay(marker); }
2.3 連線
連線實際使用的是繪制多邊形的功能,只是當只指定了兩個點時,就是一根線。同樣,這里定義一個函數以方便直接調用。
/** * 指定起止經緯度,繪制連接線 * * longitudeFrom 經度 * latitudeFrom 緯度 * longitudeTo 經度 * latitudeTo 緯度 **/ function addLine(longitudeFrom, latitudeFrom, longitudeTo, latitudeTo) { if (!map) return; var pointFrom=new BMap.Point(longitudeFrom, latitudeFrom); var pointTo=new BMap.Point(longitudeTo, latitudeTo); // 可以指定多點連接,此處只考慮兩點 var line=new BMap.Polyline([pointFrom, pointTo], { strokeWeight:1, strokeOpacity:0.5, strokeColor:"red" }); // 添加到地圖上 map.addOverlay(line); }
2.4 圖例
圖例需要以地圖定義的控件方式來添加,在控件的 initialize 事件中完成 DOM 元素的生成即可,為了體現過程本身,以下函數把 DOM 的html文本作為參數,由外部靈活定義。
/** * 添加圖例 * 實質就是在地圖上添加自己的頁面元素 * * html 網頁元素 **/ function addLegend(html){ var LegendControl=function () { this.defaultAnchor=BMAP_ANCHOR_TOP_LEFT; this.defaultOffset=new BMap.Size(10, 10); } LegendControl.prototype=new BMap.Control(); LegendControl.prototype.initialize=function (map) { var le=$(html)[0]; map.getContainer().appendChild(le); return le; }; var legendCtrl=new LegendControl(); map.addControl(legendCtrl); }
2.5 綜合
有了以上函數,綜合起來就流程清晰了。以下坐標,均通過坐標拾取器獲取。
// 機器類:經度,緯度,名稱 function Machine(longitude, latitude, name){ this.longitude=longitude; this.latitude=latitude; this.name=name; } // 確定地圖的中心位置與縮放級別 var center=new BMap.Point(110.423997,31.40979); map.centerAndZoom(center, 6); // 級別 6,跨省視圖 // 添加圖例,自由寫 html addLegend("<div style='font-size:12px; color:gray; width:140px; padding:5px; background:white; text-align:center; border:solid 1px gray;'>總部:<img src='head.png' style='width:16px; vertical-align:middle;' /> 設備:<img src='machine.png' style='width:16px; vertical-align:middle;' /></div>"); // 總部位置 var head={ longitude : 112.918702343957, latitude : 28.30070516 }; addMarker(head.longitude, head.latitude, 'head', '總部'); // 所有機器位置 var machineList=[ new Machine(114.876143,38.113315,'石家莊'), new Machine(112.521289,37.822014,'太原'), new Machine(108.989008,34.328175,'西安'), new Machine(117.230997,31.881961,'合肥'), new Machine(103.984944,30.553819,'成都'), new Machine(108.400295,22.862517,'南寧'), new Machine(113.257181,23.169067,'廣州'), new Machine(120.174565,30.298715,'杭州'), new Machine(102.881106,24.959705,'昆明') ]; // 添加所有機器并連線 for(var i=0; i<machineList.length; i++){ addMarker(machineList[i].longitude, machineList[i].latitude, 'machine', machineList[i].name); addLine(head.longitude, head.latitude, machineList[i].longitude, machineList[i].latitude); }
3. 結語
本文完整代碼可從此處下載:
https://github.com/triplestudio/helloworld/blob/master/baidu_map_demo.html
在此基礎上,可以根據需要進一步擴展功能,具體參考百度地圖開放平臺開發文檔:
http://lbsyun.baidu.com/
在上一章中,我們討論了 D3 如何使用其形狀生成器函數計算復雜形狀(如曲線、面積和弧)的 d 屬性。在本章中,我們將通過布局將這些形狀提升到另一個層次。在 D3 中,布局是將數據集作為輸入并生成新的批注數據集作為輸出的函數,其中包含繪制特定可視化效果所需的屬性。例如,餅圖布局計算餅圖每個扇區的角度,并使用這些角度批注數據集。同樣,堆棧布局計算堆積形狀在堆積條形圖或流圖中的位置。
布局不會繪制可視化效果,也不會像組件一樣調用它們,也不會像形狀生成器那樣在繪圖代碼中引用。相反,它們是一個預處理步驟,用于設置數據的格式,以便準備好以您選擇的形式顯示。
圖5.1 布局功能是用于計算繪制特定圖表所需信息的數據預處理步驟。
在本章中,我們將餅圖和堆棧布局與第 4 章中討論的弧形和面積形狀生成器相結合,以創建圖 5.2 所示的項目。您也可以在 https://d3js-in-action-third-edition.github.io/visualizing-40-years-of-music-industry-sales/ 在線找到它。該項目可視化了 1973 年至 2019 年間音樂行業每種格式的銷售情況。它的靈感來自2020年MakeoverMonday(www.makeovermonday.co.uk/week-21-2020/)舉辦的挑戰。
圖 5.2 1973 年至 2019 年音樂行業銷售的可視化。這是我們將在本章中構建的項目。
雖然本章只介紹了餅圖和堆棧布局,但其他布局,如和弦布局和更奇特的布局,遵循相同的原則,看完這些應該很容易理解。
在開始之前,請轉到第 5 章的代碼文件。您可以從本書的 Github 存儲庫下載它們(https://github.com/d3js-in-action-third-edition/code-files)。在名為 chapter_05 的文件夾中,代碼文件按節進行組織。要開始本章的練習,請在代碼編輯器中打開 5.1-Pie_layout/start 文件夾并啟動本地 Web 服務器。如果您需要有關設置本地開發環境的幫助,請參閱附錄 A。您可以在位于本章代碼文件根目錄下的自述文件中找到有關項目文件夾結構的更多詳細信息。
我們將在本章中構建的三個可視化(圓環圖、堆積條形圖和流圖)共享相同的數據、維度和比例。為了避免重復,該項目被分解為多個 JavaScript 文件,其中一個用于可視化共享的常量,另一個專門用于比例。這種方法將使我們的代碼更易于閱讀和修改。在生產代碼中,我們可能會使用 JavaScript 導入和導出來訪問不同的函數,并結合 Node 和捆綁器。在討論前端框架時,我們將到達那里,但現在,我們將堅持一個類似遺留的項目結構,以保持對 D3 的關注。請注意,D3 庫和所有 JavaScript 文件都已加載到 index.html 中。
使用本章的代碼文件時,在代碼編輯器中僅打開一個開始文件夾或一個結束文件夾。如果一次打開章節的所有文件并使用 Live Server 擴展為項目提供服務,則數據文件的路徑將無法按預期工作。
在本節中,我們將使用 D3 的餅圖布局來創建圓環圖,您可以在圖 5.2 頂部和托管項目 (https://d3js-in-action-third-edition.github.io/visualizing-40-years-of-music-industry-sales/) 上看到該圓環圖。更具體地說,我們將可視化 1975 年、1995 年和 2013 年每種音樂格式的銷售額細分。每個圓環圖的中心將對應于相應年份在流圖和下面堆疊條形圖的 x 軸上的位置。
讓我們花點時間建立一個策略,以確保每個圖表根據 x 軸上的年份正確水平對齊。一個簡單的方法是使用第4章中描述的保證金約定。隨著本章的進展,我們將使用三個 SVG 容器:一個用于圓環圖,一個用于流圖,一個用于堆積條形圖。這些容器中的每一個都具有相同的尺寸并共享相同的邊距。為內部圖表保留的區域(沒有軸和標簽的可視化效果)也將具有相同的維度并水平對齊,如圖 5.3 所示。文件 js/shared-constant.js 已包含可視化共享的邊距對象和維度常量。
我們還在 js/load-data 中為您加載了 CSV 數據文件.js .有關如何將數據加載到 D3 項目中的更多信息,請參閱第 4 章和第 3 章。加載數據后,我們調用函數 defineScales() 和 drawDonutCharts() ,我們將在本節中使用它們。
首先,讓我們為圓環圖追加一個 SVG 容器和一個定義為內部圖表保留區域的 SVG 組。為此,我們轉到 js/donut-charts.js并在函數 drawDonutCharts() 中,我們創建 SVG 容器和一個 SVG 組。在下面的代碼片段中,您將看到我們在 div 內附加了 SVG 容器,ID 為 donut 。請注意,我們通過根據圖表的左邊距和上邊距平移組來應用邊距約定。
const svg=d3.select("#donut")
.append("svg") #A
.attr("viewBox", `0 0 ${width} ${height}`); #A
const donutContainers=svg
.append("g") #B
.attr("transform", `translate(${margin.left}, ${margin.top})`); #B
您可能想知道為什么我們需要將邊距約定應用于圓環圖,因為沒有軸和標簽的帳戶空間。這是因為每個圓環圖將根據其所代表的年份水平定位。由于我們希望這些年份的水平位置與下面的流線圖和堆疊條形圖中相同,因此我們需要考慮邊際慣例。
在第 4 章中,我們討論了極坐標以及如何通過將弧包含在 SVG 組中并將該組轉換為圖表中心的位置來促進餅圖或圓環圖的創建。通過以這種方式進行,弧線將自動圍繞該中心繪制。
我們將在這里應用相同的策略,唯一的區別是我們需要考慮三個圓環圖,并且它們的中心水平位置對應于它們所代表的年份,如圖 5.4 所示。
圖 5.4 組成圓環圖的每組弧都包含在 SVG 組中。這些組根據它們所代表的年份進行水平翻譯。該位置是使用 D3 刻度計算的。
要計算每個甜甜圈中心的水平位置,我們需要一個刻度。如您所知,我們使用 D3 刻度將數據(此處為年份)轉換為屏幕屬性,此處為水平位置。線性或時間刻度對于我們的目的來說效果很好,但我們選擇波段刻度,因為我們知道我們稍后會繪制一個堆疊條形圖,它將共享相同的刻度。有關頻段刻度工作原理的更多說明,請參閱第 3 章。
在文件中 js/scale.js ,我們首先使用函數 d3.scaleBand() 初始化波段刻度,并將其存儲在名為 xScale 的常量中。請注意我們如何在函數 defineScales() 中聲明刻度的域和范圍。這種方法讓我們等到數據加載完成,然后再嘗試使用它來設置域(一旦數據準備就緒,函數 defineScales() 從加載數據調用.js)。我們在函數外部聲明常量 xScale,使其可以從其他 js 文件訪問。
const xScale=d3.scaleBand(); #A
const defineScales=(data)=> {
xScale
.domain(data.map(d=> d.year)) #B
.range([0, innerWidth]); #B
};
帶狀刻度接受離散輸入作為域,并返回該范圍的連續輸出。在清單 5.1 中,我們使用 JavaScript map() 方法,通過每年從數據集創建一個數組來設置域。對于范圍,我們傳遞一個數組,其中包含可用水平空間的最小值(零)和最大值(對應于內部圖表的 innerWidth)。
我們回到函數 drawDonutCharts() ,正如你在清單 5.2 中看到的,我們首先聲明一個名為 years 的數組,它列出了我們感興趣的年份,這里是 1975、1995 和 2013。然后,使用 forEach() 循環,我們為感興趣的每一年附加一個 SVG 組,并將其保存在名為 donutContainer 的常量中。最后,我們通過設置組的轉換屬性來翻譯組。水平平移是通過調用計算的 xScale ,我們將當前年份傳遞到該平移,而垂直平移對應于內部圖表的半高。
const years=[1975, 1995, 2013];
years.forEach(year=> {
const donutContainer=donutContainers
.append("g")
.attr("transform", `translate(${xScale(year)}, ${innerHeight/2})`);
});
完成準備步驟后,我們現在可以專注于圓環圖。餅圖和圓環圖可視化部分與整體的關系或每個扇區相對于總量表示的數量。D3 餅圖布局生成器通過根據每個切片所代表的百分比計算每個切片的開始和結束角度來幫助我們。
D3 的餅圖生成器希望輸入數據格式化為數字數組。例如,對于 1975 年,我們可以有一個數組,其中包含與每種音樂格式對應的銷售額,如下所示:
const sales1975=[8061.8, 2770.4, 469.5, 0, 0, 0, 48.5];
雖然這樣一個簡單的數組足以生成餅圖,但它會阻止我們以后根據它所代表的音樂格式為每個切片分配顏色。為了隨身攜帶這些信息,我們可以使用一個對象數組,其中包含音樂格式的 ID 和感興趣年份的相關銷售額。
在示例 5.3 中,我們首先從加載數據集的 columns 屬性中提取格式。獲取數據時,例如,使用 d3.csv() 方法,D3 將一個數組附加到數據集,其中包含原始 CSV 數據集中每列的標題,并使用鍵 data.columns 進行訪問。如果將提取的數據記錄到控制臺中,則會在數據數組的末尾看到它,如圖 5.5 所示。
由于我們只對音樂格式感興趣,因此我們可以過濾列數組以刪除“year”標簽。
圖 5.5 從 CSV 文件獲取數據時,D3 將數組附加到數據集,其中包含原始數據集中列的標題。可以使用鍵 data.columns 訪問此數組。
為了準備餅圖生成器的數據,我們還需要提取感興趣的年份的數據。我們使用 JavaScript 方法 find() 隔離這些數據,并將其存儲在名為 yearData 的常量中。
我們遍歷格式數組,對于每種格式,我們創建一個對象,其中包含格式 id 及其感興趣年份的相關銷售額。最后,我們將這個對象推入 數組格式化數據 ,之前聲明。
const years=[1975, 1995, 2013];
const formats=data.columns.filter(format=> format !=="year"); #A
years.forEach(year=> {
...
const yearData=data.find(d=> d.year===year); #B
const formattedData=[]; #C
formats.forEach(format=> { #D
formattedData.push({ format: format, sales: yearData[format] }); #D
}); #D
});
準備就緒后,格式化數據是一個對象數組,每個對象都包含格式的 id 及其感興趣年份的相關銷售額。
//=> formattedData=[
{ format: "vinyl", sales: 8061.8 },
{ format: "eight_track", sales: 2770.4 },
{ format: "cassette", sales: 469.5 },
{ format: "cd", sales: 0 },
{ format: "download", sales: 0 },
{ format: "streaming", sales: 0 },
{ format: "other", sales: 48.5 }
];
現在數據格式正確,我們可以初始化餅圖布局生成器。我們用方法 d3.pie() 構造一個新的餅圖生成器,它是 d3 形狀模塊 (https://github.com/d3/d3-shape#pies) 的一部分。由于格式化數據是一個對象數組,我們需要告訴餅圖生成器哪個鍵包含將決定切片大小的值。我們通過設置 value() 訪問器函數來做到這一點,如以下代碼片段所示。我們還將 pie 生成器存儲在一個名為 pieGenerator 的常量中,以便我們可以像調用任何其他函數一樣調用它。
const pieGenerator=d3.pie()
.value(d=> d.sales);
要生成餅圖布局的數據,我們只需調用餅圖生成器函數,將格式化的數據作為參數傳遞,并將結果存儲在名為 注釋數據 .
const pieGenerator=d3.pie()
.value(d=> d.sales);
const annotatedData=pieGenerator(formattedData);
餅圖生成器返回一個新的帶批注的數據集,其中包含對原始數據集的引用,但也包括新屬性:每個切片的值、其索引及其開始和結束角度(以弧度為單位)。請注意,每個切片之間的填充也包括 padAngle 并且當前設置為零。我們稍后會改變這一點。
//=> annotatedData=[
{
data: { format: "vinyl", sales: 8061.8 },
value: 8061.8,
index: 0,
startAngle: 0,
endAngle: 4.5,
padAngle: 0,
},
...
];
請務必了解餅圖布局生成器不直接參與繪制餅圖。這是一個預處理步驟,用于計算餅圖扇區的角度。如圖5.1和5.6所述,此過程通常包括三個步驟:
圖 5.6 餅圖布局生成器是一個預處理步驟,用于生成一個帶注釋的數據集,其中包含餅圖每個切片的開始和結束角度。該過程通常涉及格式化我們的數據,初始化餅圖生成器函數,并調用該函數以獲取帶注釋的數據。
準備好帶注釋的數據集后,是時候生成弧線了!您將看到以下步驟與上一章中創建弧的方式非常相似。出于這個原因,我們不會解釋每一個細節。如果您需要更深入的討論,請參閱第 4 章。
在示例 5.4 中,我們首先通過調用 d3.arc() 方法及其負責設置圖表內外半徑、切片之間的填充以及切片角半徑的各種訪問器函數來初始化 arc 生成器。如果內半徑設置為零,我們將獲得一個餅圖,而如果它大于零,我們將得到一個圓環圖。
與第 4 章中使用的策略的唯一區別是,這次我們可以在聲明電弧發生器的同時設置 startAngle() 和 endAngle() 訪問器函數。這是因為現在,這些值包含在帶注釋的數據集中,我們可以告訴這些訪問器函數如何通過 d.startAngle 和 d.endAngle .
要使弧出現在屏幕上,我們需要做的最后一件事是使用數據綁定模式為注釋數據集中的每個對象生成一個路徑元素(每個弧或切片都有一個對象)。請注意,在清單 5.4 中,我們如何為每個甜甜圈的弧指定一個特定的類名 ( 'arc-${year}' ),并將該類名用作數據綁定模式中的選擇器。由于我們正在循環中創建甜甜圈,這將防止 D3 在制作新甜甜圈時覆蓋每個甜甜圈。
最后,我們調用弧發生器函數來計算每條路徑的 d 屬性。
const arcGenerator=d3.arc()
.startAngle(d=> d.startAngle) #A
.endAngle(d=> d.endAngle) #A
.innerRadius(60)
.outerRadius(100)
.padAngle(0.02)
.cornerRadius(3);
const arcs=donutContainer
.selectAll(`.arc-${year}`) #B
.data(annotatedData) #B
.join("path") #B
.attr("class", `arc-${year}`)
.attr("d", arcGenerator); #C
如果您保存項目并在瀏覽器中查看圓環圖,您會發現它們的形狀是正確的,但每個弧線都是漆黑的。這是正常的,黑色是 SVG 路徑的默認填充屬性。為了提高可讀性,我們將根據每個弧線所代表的音樂格式對它們應用不同的顏色。
將正確的顏色應用于每個弧的一種簡單且可重用的方法是聲明色階。在 D3 中,色階通常使用 d3.scaleOrdinal() (https://github.com/d3/d3-scale#scaleOrdinal) 創建。序數刻度將離散域映射到離散范圍。在我們的例子中,域是音樂格式的數組,范圍是包含與每種格式關聯的顏色的數組。
在文件比例中.js ,我們首先聲明一個序數比例并將其保存在常量色階中。然后,我們通過將 formatInfo 數組(在共享常量中可用.js的每個格式 id 映射到數組中來設置其域。我們對顏色做同樣的事情,您可以根據自己的喜好進行個性化設置。在本章中,我們將重用此色階來創建構成我們項目的所有圖表。
const colorScale=d3.scaleOrdinal();
const defineScales=(data)=> {
colorScale
.domain(formatsInfo.map(f=> f.id))
.range(formatsInfo.map(f=> f.color));
};
回到圓環圖.js我們可以通過將綁定到每個弧的音樂格式 id 傳遞給色階來設置弧的填充屬性。
const arcs=donutContainer
.selectAll(`.arc-${year}`)
.data(annotatedData)
.join("path")
.attr("class", `arc-${year}`)
.attr("d", arcGenerator)
.attr("fill", d=> colorScale(d.data.format));
保存您的項目并在瀏覽器中查看。看起來還不錯!弧線已按降序顯示,從最大到最小,這有助于提高可讀性。我們已經可以看到音樂的面貌在 1975 年、1995 年和 2013 年間發生了怎樣的變化,主導格式完全不同。
圖 5.7 1975年、1995年和2013年的圓環圖
在第4章中,我們提到餅圖有時很難解釋,因為人腦不太擅長將角度轉換為比率。我們可以通過在圓環圖的質心上添加一個標簽來提高圓環圖的可讀性,該標簽以百分比表示每個弧的值,就像我們在上一章中所做的那樣。
在示例 5.5 中,我們稍微修改了用于創建圓弧的代碼(來自示例 5.4)。首先,我們使用數據綁定模式來追加 SVG 組而不是路徑元素。然后,我們將路徑元素(用于圓弧)和 SVG 文本元素(用于標簽)附加到這些組中。由于父母將綁定數據傳遞給孩子,因此我們將在塑造弧線和標簽時訪問數據。
我們通過調用電弧發生器來繪制電弧,就像我們之前所做的那樣。要設置標簽的文本,我們需要計算每個弧線表示的比率或百分比。我們通過從弧的結束角度中減去弧的起始角并將結果除以 2π(以弧度為單位的完整圓覆蓋的角度)來執行此計算。請注意我們如何使用括號表示法(d[“百分比”])將百分比值存儲到綁定數據中。當我們需要對不同的屬性進行相同的計算時,這個技巧很有用。它可以防止您多次重復計算。為了返回標簽的文本,我們將計算出的百分比傳遞給方法 d3.format(“.0%”) ,該方法生成一個舍入百分比并在標簽末尾添加一個百分比符號。
我們應用相同的策略來計算每個弧的質心,這是我們想要放置標簽的位置。當設置標簽的 x 屬性時,我們計算相關弧的質心(使用第 4 章中討論的技術)并將其存儲在綁定數據中( d[“質心”])。然后,在設置 y 屬性時,質心數組已經可以通過 d.centroid 訪問。
為了使標簽水平和垂直地以質心居中,我們需要將其文本錨點和主要基線屬性設置為中間。我們還使用fill屬性將它們的顏色設置為白色,將其字體大小增加到16px,將其字體粗細增加到500以提高可讀性。
如果您保存項目并在瀏覽器中查看圓環圖,您會發現標簽在大弧上工作良好,但在較小的弧線上幾乎無法讀取。在專業項目中,我們可以通過將小弧的標簽移動到圓環圖之外來解決這個問題。對于此項目,當百分比小于 5% 時,我們根本不會通過將其填充不透明度屬性設置為零來顯示這些標簽。
const arcs=donutContainer
.selectAll(`.arc-${year}`)
.data(annotatedData)
.join("g") #A
.attr("class", `arc-${year}`);
arcs #B
.append("path") #B
.attr("d", arcGenerator) #B
.attr("fill", d=> colorScale(d.data.format)); #B
arcs
.append("text") #C
.text(d=> {
d["percentage"]=(d.endAngle - d.startAngle) / (2 * Math.PI); #D
return d3.format(".0%")(d.percentage); #D
})
.attr("x", d=> { #E
d["centroid"]=arcGenerator #E
.startAngle(d.startAngle) #E
.endAngle(d.endAngle) #E
.centroid(); #E
return d.centroid[0]; #E
}) #E
.attr("y", d=> d.centroid[1]) #E
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("fill", "#f6fafc")
.attr("fill-opacity", d=> d.percentage < 0.05 ? 0 : 1) #F
.style("font-size", "16px")
.style("font-weight", 500);
圖 5.8 帶百分比標簽的圓環圖
作為最后一步,我們將指示圓環圖所代表的年份,標簽位于其中心。我們通過在每個甜甜圈容器中附加一個文本元素來做到這一點。因為我們還在循環往復年份,所以我們可以直接應用當前年份作為標簽的文本。此外,由于圓環容器位于圖表的中心,因此文本元素會自動正確定位。我們所要做的就是設置其文本錨點和主要基線屬性,使其水平和垂直居中。
donutContainer
.append("text")
.text(year)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.style("font-size", "24px")
.style("font-weight", 500);
瞧!我們的圓環圖是完整的。
圖 5.9 帶有年份標簽的完整圓環圖
圖 5.10 回顧了創建餅圖或圓環圖的步驟。在第一步中,我們使用布局函數 d3.pie() 預處理數據,以獲得帶有注釋的數據集,其中包含每個切片的角度。然后,我們使用弧發生器函數繪制弧線,該函數從注釋數據集中獲取角度并返回每個路徑的 d 屬性。最后,我們使用 SVG 文本元素添加標簽以提高圖表的可讀性。
圖 5.10 創建餅圖或圓環圖所涉及的主要步驟。
到目前為止,我們已經處理了可以在任何傳統電子表格中輕松創建的信息可視化的簡單示例。但是你進入這個行業并不是為了制作類似Excel的圖表。您可能希望用漂亮的數據讓您的觀眾驚嘆不已,為您的美學 je ne sais quoi 贏得獎項,并通過您隨著時間的推移而變化的表現喚起深刻的情感反應。
流圖是代表變化和變化的崇高信息可視化。在你開始把這些部分放在一起之前,創作似乎具有挑戰性。歸根結底,流圖是所謂的堆積面積圖的變體。這些層相互累積,并根據靠近中心的組件所占用的空間來調整上方和下方元素的面積。它似乎是有機的,因為這種吸積性模仿了許多生物的生長方式,似乎暗示了控制生物生長和衰敗的各種涌現特性。我們稍后會解釋它的外觀,但首先,讓我們弄清楚如何構建它。
我們在本書的第一部分看了一個流線圖,因為它實際上并沒有那么奇特。流圖是一種堆積圖,這意味著它與堆積條形圖基本相似,如圖 5.11 所示。流線圖也類似于我們在上一章中構建的折線圖后面的區域,只是這些區域相互堆疊。在本節中,我們將使用 D3 的堆棧和面積生成器來創建堆疊條形圖,然后創建流線圖。
圖 5.11 流圖與堆積條形圖基本相似。在 D3 中,兩者都是使用堆棧布局生成器創建的。
在 D3 中,創建堆積條形圖或流圖的步驟類似,如圖 5.12 所示。首先,我們初始化一個堆棧布局生成器并設置堆棧的參數。然后,我們將原始數據集傳遞給堆棧生成器,堆棧生成器將返回一個新的注釋數據集,指示每個數據點的下限和上限。如果我們制作一個流線圖,我們還必須初始化一個面積生成器,類似于上一章中討論的直線和曲線生成器。最后,我們將帶注釋的數據集綁定到制作圖表所需的 SVG 形狀、堆疊條形圖的矩形或流圖的路徑。在流圖的情況下,調用面積生成器來計算路徑的 d 屬性。我們將在以下小節中更詳細地介紹這些步驟。
圖 5.12 使用 D3 創建堆積圖的步驟。
堆棧布局生成器是一個 D3 函數,它將具有多個類別的數據集作為輸入。本章示例中使用的數據集包含 1973 年至 2019 年間每年不同音樂格式的總銷售額。每種音樂格式將成為堆疊圖表中的一個系列。
與前面討論的餅圖布局生成器一樣,堆棧布局函數返回一個新的注釋數據集,其中包含不同序列在“堆疊”到另一個時的位置。堆棧生成器是 d3 形狀模塊 (https://github.com/d3/d3-shape#stacks) 的一部分。
讓我們將堆棧布局付諸行動,并開始在位于堆疊條形圖中的函數 drawStackedBars() 中工作.js 。請注意,此函數已經包含將 SVG 容器附加到 div 的代碼,ID 為 “bars”,以及內部圖表的組容器。這與我們在第4章中使用的策略相同,與保證金慣例并行。
在下面的代碼片段中,我們首先使用方法 d3.stack() 聲明一個堆棧生成器,并將其存儲在一個名為 stackGenerator 的常量中。然后,我們需要告訴生成器數據集中的哪些鍵包含我們要堆疊的值(將成為序列)。我們使用 keys() 訪問器函數來做到這一點,我們將類別 id 數組傳遞給該函數,這里是每種音樂格式的標識符。我們通過映射 formatInfo 常量的 id 來創建這個數組。我們還可以使用附加到數據集的列鍵并過濾掉年份,就像我們在 5.1.2 節中所做的那樣。
最后,我們調用堆棧生成器并將數據作為參數傳遞,以獲得帶注釋的數據集。我們將新數據集存儲在名為 注釋數據 .
const stackGenerator=d3.stack() #A
.keys(formatsInfo.map(f=> f.id)); #B
const annotatedData=stackGenerator(data); #C
如果將帶注釋的數據集記錄到控制臺中,您將看到它由多維數組組成。我們首先為每個系列提供一個數組,如圖 5.13 所示,序列的 id 可通過 key 屬性獲得。然后,序列數組包含另一組數組,數據集中每年一個數組。最后這些數組包括相關年份類別的下限和上限以及該年份的原始數據。下限和上限分別由索引 d[0] 和 d[1] 訪問,如果 d 對應于數組。
格式“乙烯基”是堆棧布局處理的第一個鍵。請注意,它的下限始終為零,而其上邊界對應于該格式的當年銷售額。然后,以下類別是“8 軌”。8 軌的下邊界對應于黑膠唱片的上邊界,我們將 8 軌的銷量相加以獲得其上限,從而創建一個堆棧。
圖 5.13 堆棧布局生成器返回的帶注釋的數據集。
如果“堆棧”的概念還不清楚,下圖可能會有所幫助。如果我們從原始數據集中仔細觀察 1986 年,我們將看到音樂主要通過三種格式提供:黑膠唱片的銷量為 2,825M$,盒式磁帶為 5,830M$,CD 為 2,170M$。我們在圖5.14的左側顯示了這些數據點,獨立繪制。
當我們使用堆棧布局時,我們創建所謂的“數據列”而不是“數據點”,每列都有下限和上限。如果我們的堆棧從黑膠唱片開始,則下限為零,上邊界對應于 1986 年黑膠唱片的銷售額:2,825M$。然后,我們將盒式磁帶的銷售疊加在其上:下邊界對應于黑膠唱片的上限(2,825M$),上邊界是黑膠唱片和盒式磁帶(8,655M$)的銷售量。這個上邊界成為CD銷售的下限,其上邊界對應于三種格式(10,825M$)的銷售量相加。這些邊界在帶注釋的數據集中通過索引(d[0]和d[1])訪問。
圖 5.14 堆棧布局生成器將數據點轉換為堆疊數據列,并返回包含每個數據列的下限和上限的帶注釋的數據集。在這里,我們看到 1986 年的一個例子。
在本節中,我們將創建您在圖 5.11 底部看到的堆積條形圖。堆積條形圖類似于我們在第 2 章和第 3 章中已經制作的條形圖,只是條形圖分為多個類別或系列。堆積條形圖和一般的堆積可視化通常用于顯示趨勢隨時間推移的演變。
就像我們對圓環圖所做的那樣,我們將使用堆棧布局返回的帶注釋的數據集來繪制對應于每個類別的條形。但首先,我們需要一個垂直軸的比例,將每個矩形的下邊界和上邊界轉換為垂直位置。我們希望條形的高度與銷售額成線性比例,因此我們將使用線性刻度。由于此刻度需要訪問帶注釋的數據,因此我們將在函數 drawStackedBars() 中聲明它。
刻度域從零到注釋數據中可用的最大上限。我們知道,這個最大值必須存在于最后一個帶注釋的數據系列中,這些數據將位于圖表的頂部。我們可以使用 length 屬性訪問這個系列( annotatedData[annotatedData.length - 1])。然后,我們使用方法 d3.max() 檢索屬性 d[1] 下的最大值,該值對應于上限。
垂直刻度的范圍從內部圖表底部的innerHeight到內部圖表頂部的零(請記住,SVG垂直軸在向下方向上為正)。最后,我們將 scale 聲明與方法 .nice() 鏈接起來,這將確保域以“nice”舍入值結束,而不是注釋數據集中的實際最大值。
const maxUpperBoundary=d3.max(annotatedData[annotatedData.length - 1], d
?=> d[1]);
const yScale=d3.scaleLinear()
.domain([0, maxUpperBoundary])
.range([innerHeight, 0])
.nice();
我們現在已準備好附加條形圖。為此,我們遍歷帶注釋的數據,并逐個附加序列,如清單 5.7 中所述。我們從數據綁定模式開始,為系列數組中的每個項目或年份附加一個矩形元素(每種音樂格式都有一個系列)。請注意我們如何將與當前系列相關的類名應用于矩形并將其用作選擇器。如果我們簡單地使用“rect”元素作為選擇器,每次執行循環時,先前創建的矩形都將被刪除并替換為新矩形。
然后,我們通過調用帶刻度的帶寬屬性來設置矩形的 x 屬性,通過將當前年份傳遞給 xScale 來設置它們的寬度屬性。y 屬性對應于矩形左上角的垂直位置,由前面聲明的垂直刻度返回,我們將矩形的上邊界 (d[1] ) 傳遞到該刻度。
同樣,矩形的高度是其上邊界和下邊界位置之間的差異。這里有一點問題。因為 SVG 垂直軸在向下方向上是正的,所以 yScale(d[0]) 返回的值高于 yScale(d[1])。我們需要從前者中減去后者,以避免為 y 屬性提供負值,這會引發錯誤。
最后,我們通過將當前音樂格式傳遞給色階來設置 fill 屬性,該色階可在每個系列的 key 屬性下訪問,如前面圖 5.13 所示。
annotatedData.forEach(serie=> { #A
innerChart
.selectAll(`.bar-${serie.key}`) #B
.data(serie) #B
.join("rect") #B
.attr("class", d=> `bar-${serie.key}`) #B
.attr("x", d=> xScale(d.data.year)) #C
.attr("y", d=> yScale(d[1])) #C
.attr("width", xScale.bandwidth()) #C
.attr("height", d=> yScale(d[0]) - yScale(d[1])) #C
.attr("fill", colorScale(serie.key)); #C
});
如果保存項目,您將看到條形之間沒有水平空間。我們可以通過回到 xScale 的聲明來解決這個問題,并將其 paddingInner() 訪問器函數設置為值 20%,就像我們在第 3 章中所做的那樣。
xScale
.domain(data.map(d=> d.year))
.range([0, innerWidth])
.paddingInner(0.2);
為了完成我們的堆積條形圖,我們需要添加軸。在清單 5.9 中,我們首先使用方法 d3.axisBottom() 聲明一個底部軸,并將 xScale 作為引用傳遞。
我們將軸聲明與方法鏈接起來, .tickValues() ,它允許我們陳述我們希望在圖表上看到的確切刻度和標簽。否則,D3 將每年提供一對刻度和標簽,看起來會很局促且難以閱讀。方法.tickValues()將值數組作為參數。我們使用方法 d3.range() 生成這個數組,并聲明我們想要從 1975 年到 2020 年的每個整數,步長為 5。
我們還使用方法 .tickSizeOuter() 隱藏底部軸兩端的刻度,我們向其傳遞值為零。方法tickValues()和tickSizeOuter()都可以在d3軸模塊(https://github.com/d3/d3-axis)中找到,而d3.range()是d3-array模塊(https://github.com/d3/d3-array)的一部分。
最后,我們使用 call() 方法將底部軸附加到圖表中,在轉換為底部的組中,并對左軸執行相同的操作。
const bottomAxis=d3.axisBottom(xScale) #A
.tickValues(d3.range(1975, 2020, 5)) #A
.tickSizeOuter(0); #A
innerChart #B
.append("g") #B
.attr("transform", `translate(0, ${innerHeight})`) #B
.call(bottomAxis); #B
const leftAxis=d3.axisLeft(yScale); #C
innerChart #C
.append("g") #C
.call(leftAxis); #C
如果保存項目并在瀏覽器中查看它,您可能會發現軸標簽有點太小。此外,如第 4 章所述,D3 將字體族“sans-serif”應用于包含軸元素的 SVG 組,這意味著項目的字體系列不會被繼承。從 CSS 文件可視化中.css ,我們可以使用選擇器 .tick 文本定位軸標簽并修改其樣式屬性。在下面的代碼片段中,我們更改了它們的字體系列、字體大小和字體粗細屬性。
.tick text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: 500;
}
完成后,堆積條形圖將類似于圖 5.15 中的條形圖,但看起來還不像圖 5.2 中的條形圖或托管項目 (https://d3js-in-action-third-edition.github.io/visualizing-40-years-of-music-industry-sales/) 中的條形圖。我們一會兒就到那里。
圖5.15 第一版堆積條形圖
在上一小節中,我們使用堆棧布局函數生成一個帶注釋的數據集,從中繪制堆疊條形圖的矩形。現在,我們將應用類似的策略來繪制流圖。盡管流圖看起來比堆積條形圖更復雜,但它們很容易在 D3 中創建。主要區別在于,對于流圖,我們使用帶注釋的數據集來追加區域,而為堆疊條形圖附加矩形。
在本小節中,我們將使用函數 drawStreamGraph() ,您可以在文件流圖中找到它.js 。此函數已包含將 SVG 容器附加到 div 的代碼,ID 為 “streamgraph”,以及內部圖表的組容器。這與我們在第4章中使用的策略相同,與保證金慣例并行。
在示例 5.10 中,我們初始化堆棧生成器并調用它來獲取帶注釋的數據。我們還聲明了一個線性刻度來計算垂直邊界的位置。這與我們用于堆積條形圖的代碼完全相同。現在,不要擔心我們正在復制代碼。我們將在下一小節中回到它。
const stackGenerator=d3.stack() #A
.keys(formatsInfo.map(f=> f.id)); #A
const annotatedData=stackGenerator(data); #A
const maxUpperBoundary=d3.max(annotatedData[annotatedData.length - 1], d
?=> d[1]);
const yScale=d3.scaleLinear() #B
.domain([0, maxUpperBoundary]) #B
.range([innerHeight, 0]) #B
.nice(); #B
為了繪制堆疊區域,我們需要一個區域生成器函數,該函數將負責計算用于繪制序列的每個路徑元素的 d 屬性。如第4章所述,面積生成器至少使用三個訪問器函數,在我們的例子中,一個用于檢索每個數據點的水平位置,一個用于堆疊區域的下邊界,另一個用于它們的上邊界。圖 5.16 說明了面積生成器如何應用于堆疊區域。
圖 5.16 面積生成器 d3.area() 與三個或更多訪問器函數組合在一起。當與流圖的堆棧布局結合使用時,它使用每個數據點的下限和上限(y0 和 y1)來計算區域的 d 屬性。
在下面的代碼片段中,我們初始化了區域生成器 d3.area() 。首先,我們使用 x() 訪問器函數來計算每個數據點的水平位置。由于 xScale 是波段刻度,因此它返回相關年份的每個波段開頭的位置,該位置可在每個數據點的數據對象中的注釋數據集中訪問 ( d.data.year)。如果我們希望數據點與下面堆疊條形圖的條形中心水平對齊,我們需要將數據點向右平移,寬度為條形寬度的一半,我們可以用帶寬()屬性計算帶刻度。
然后,我們使用 y0() 和 y(1) 訪問器函數來確定數據點沿每個序列的下邊界和上邊界的垂直位置。這個位置是用 yScale 計算的,之前聲明了,我們將邊界的值傳遞給邊界值,可以通過邊界數據中的數組索引訪問:d[0] 表示下邊界,d[1] 表示上限邊界。
最后,如果我們想沿每個邊界插值數據點以獲得曲線而不是直線,我們使用 curve() 訪問器函數。這里我們選擇了曲線插值函數d3.curveCatmullRom。如前所述,曲線插值會修改數據的表示,必須謹慎選擇。有關討論和演示,請參閱第 4.2.2 節。
const areaGenerator=d3.area()
.x(d=> xScale(d.data.year) + xScale.bandwidth()/2)
.y0(d=> yScale(d[0]))
.y1(d=> yScale(d[1]))
.curve(d3.curveCatmullRom);
現在,我們已準備好繪制堆疊區域!首先,我們使用數據綁定模式為注釋數據集中的每個序列生成一個 SVG 路徑元素。我們調用面積生成器函數來獲取每個路徑的 d 屬性,以及它們的填充屬性的色階。
請注意我們如何在 SVG 組中附加路徑以保持標記井井有條且易于檢查。這也將有助于在以后保持區域和垂直網格的適當并置。
innerChart
.append("g")
.attr("class", "areas-container")
.selectAll("path")
.data(annotatedData)
.join("path")
.attr("d", areaGenerator)
.attr("fill", d=> colorScale(d.key));
在本節中,我們要做的最后一件事是向流圖添加軸和標簽。我們開始聲明軸生成器 d3.axisLeft() 并將 yScale 作為引用傳遞。然后,我們使用 .call() 方法將軸元素附加到 SVG 組中。
const leftAxis=d3.axisLeft(yScale);
innerChart
.append("g")
.call(leftAxis);
我們可能會省略 x 軸,因為流圖與下面的堆疊條形圖水平對齊,并且此圖表具有相同的 x 軸。但我們將利用這個機會討論如何擴展軸上的刻度以在圖表后面創建網格。
首先,我們需要記住,SVG 元素是按照它們在 SVG 容器中出現的順序繪制的。因此,如果我們希望網格出現在流線圖后面,我們需要先繪制它。這就是為什么以下代碼片段應位于追加流圖路徑的代碼片段之前的原因。
到目前為止,生成底部軸的代碼與用于堆疊條形圖的代碼相同,包括 tickValues() 和 tickSizeOuter() 方法的使用。
const bottomAxis=d3.axisBottom(xScale)
.tickValues(d3.range(1975, 2020, 5))
.tickSizeOuter(0);
innerChart
.append("g")
.attr("class", "x-axis-streamgraph")
.attr("transform", `translate(0, ${innerHeight})`)
.call(bottomAxis);
要將即時報價轉換為網格,我們所要做的就是使用 tickSize() 方法擴展它們的長度。通過這種方法,我們給即時報價一個對應于內部圖表高度的長度,乘以 -1 使它們向上增長。請注意,我們還可以首先避免平移軸,并將此長度設置為正值,以使刻度線從上到下的方向增長。每當需要水平網格時,此方法也可以應用于左軸或右軸。
const bottomAxis=d3.axisBottom(xScale)
.tickValues(d3.range(1975, 2020, 5))
.tickSizeOuter(0)
.tickSize(innerHeight * -1);
最后,我們可以選擇隱藏軸底部的水平線和年份標簽,方法是將它們的不透明度定為零。為此,我們使用之前賦予 x 軸容器的類名( x-axis-streamgraph ),并將其用作 CSS 文件可視化中的選擇器.css .正如您在下面的代碼片段中看到的,通過“ .x-axis-streamgraph path”訪問的水平線的不透明度是用stroke-opacity屬性管理的,而我們需要使用填充不透明度來隱藏年份標簽(“ .x-axis-streamgraph文本”)。我們還可以使用 D3 style() 方法來處理流圖內的不透明度.js .
.x-axis-streamgraph path {
stroke-opacity: 0;
}
.x-axis-streamgraph text {
fill-opacity: 0;
}
最后,我們將在左側軸上方添加一個標簽,以指示此軸所代表的內容。如圖 5.2 所示,或者在托管項目 (https://d3js-in-action-third-edition.github.io/visualizing-40-years-of-music-industry-sales/) 上,流圖的標簽分為兩行,第一行帶有文本“總收入(百萬美元)”,第二行提到“根據通貨膨脹進行調整”。
我們將使用 SVG 文本構建此標簽。關于 SVG 文本,需要了解的一件事是它的行為不像 HTML 文本。例如,如果我們在 HTML 元素中添加文本,文本將根據水平可用空間自動換行或重排。SVG 文本不會這樣做,每個文本元素的位置需要單獨處理。
要操作 SVG 文本中的潛臺詞,我們可以使用 tspan 元素。將文本分解為多個 tspan,允許使用其 x、y、dx 和 dy 屬性分別調整其樣式和位置,前兩個用于參考 SVG 容器的坐標系,后兩個用于參考前一個文本元素。
在上述所有定義中,請務必記住,文本基線由其文本錨點屬性水平控制,垂直由其主基線屬性控制。
為了創建我們的標簽,我們可以使用位于 SVG 文本中的三個 tspan 元素,如圖 5.17 所示。如果文本元素的主基線屬性設置為 hang ,則文本將顯示在 SVG 容器原點的正下方和右側。使用 dx 和 dy,我們可以根據圖 5.17 分別將第二個和第三個跨度移動到它們的正確位置。
圖 5.17 tspan 元素允許分別操作副詞項的樣式和位置。我們使用屬性 dx 和 dy 來設置相對于前一個文本元素的位置。
在下面的代碼片段中,我們將該策略付諸行動。首先,我們將一個文本元素附加到我們的 SVG 容器中,并將其主基線屬性設置為值 hang ,這意味著文本及其子文本的基線將位于它們的正上方。
我們將文本選擇保存到常量 leftAxisLabel 中,并重復使用它將三個 tspan 元素附加到文本容器中。我們將第一個tspan的文本設置為“總收入”,第二個tspan設置為“(百萬美元)”,第三個tspan設置為“經通貨膨脹調整”。
默認情況下,tspan 元素一個接一個地顯示在同一水平線上。保存您的項目并查看標簽進行確認。
const leftAxisLabel=svg
.append("text")
.attr("dominant-baseline", "hanging");
leftAxisLabel
.append("tspan")
.text("Total revenue");
leftAxisLabel
.append("tspan")
.text("(million USD)");
leftAxisLabel
.append("tspan")
.text("Adjusted for inflation");
要將第二個 tspan 稍微向右移動,我們可以設置其 dx 屬性并為其指定值 5。要將第三個 tspan 移動到第一個和第二個 tspan 下方,我們可以使用 y 或 dy 屬性并為其指定值“20”。在這種特殊情況下,這兩個屬性將具有相同的效果。最后,如果我們希望第三個 tspan 的左側與 SVG 容器的左邊框對齊,最好使用 x 屬性并將其設置為零。
const leftAxisLabel=svg
.append("text")
.attr("dominant-baseline", "hanging");
leftAxisLabel
.append("tspan")
.text("Total revenue");
leftAxisLabel
.append("tspan")
.text("(million USD)")
.attr("dx", 5);
leftAxisLabel
.append("tspan")
.text("Adjusted for inflation")
.attr("x", 0)
.attr("dy", 20);
通常,tspan 元素用于將不同的樣式應用于文本的一部分。例如,我們可以降低第二個和第三個 tspan 元素的不透明度,使它們呈灰色,并減小第三個 tspan 的字體大小,因為與標簽的其余部分相比,它傳達了次要信息。
const leftAxisLabel=svg
.append("text")
.attr("dominant-baseline", "hanging");
leftAxisLabel
.append("tspan")
.text("Total revenue");
leftAxisLabel
.append("tspan")
.text("(million USD)")
.attr("dx", 5)
.attr("fill-opacity", 0.7);
leftAxisLabel
.append("tspan")
.text("Adjusted for inflation")
.attr("x", 0)
.attr("dy", 20)
.attr("fill-opacity", 0.7)
.style("font-size", "14px");
我們的流圖的第一次迭代現在已經完成,如圖 5.18 所示。當此類圖表的垂直基線位于零時,我們通常將其命名為堆積面積圖,而流線圖的面積往往位于中心基線周圍。在下一小節中,我們將討論如何更改圖表的基線。但在我們到達那里之前,觀察堆疊條形圖和堆疊面積圖在這一點上的相似之處很有趣。
圖 5.18 我們流線圖的第一次迭代,也可以命名為堆積面積圖。
通過控制序列的堆疊順序以及它們在零基線周圍的垂直定位方式,我們可以將堆積條形圖和堆積面積圖更進一步。此級別的控制是通過 order() 和 offset() 訪問器函數實現的,這兩個函數都應用于堆棧布局生成器。
讓我們首先看一下 order() 訪問器函數,它控制形狀垂直堆疊的順序。D3 有六個內置訂單,可以作為參數傳遞,如圖 5.19 所示。
d3.stackOrderNone 是默認順序,這意味著如果未設置 order() 訪問器函數,則應用該順序。它按與 keys 數組中列出的順序相同的順序堆疊對應于每個系列的形狀,從下到上。d3.stackOrderReverse顛倒了這個順序,從底部的最后一個鍵開始,到頂部的第一個鍵結束。
d3.stackOrderAscending 計算每個序列的總和。總和最小的序列位于底部,其他序列按升序堆疊。同樣,d3.stackOrder降序將總和最大的序列放在底部,并按降序堆疊序列。
最后兩個訂單計算每個序列達到其最大值的指數。d3.stackOrderAppearance 按序列達到峰值的順序堆疊序列,這對于可讀性非常有用,尤其是對于基線為零的堆棧。另一方面,d3.stackOrderInsideOut 將峰值最早的序列定位在圖表的中間,將最新峰值的序列放在外面。此順序非常適合形狀圍繞中心基線分布的流線圖。
圖 5.19 D3 允許使用 order() 訪問器函數控制形狀堆疊的順序。在這里,我們看到堆積區域的示例,但相同的原則適用于堆積條形圖。
堆棧布局的另一個訪問器函數稱為 offset() ,控制圖表零基線的位置以及形狀在其周圍的分布方式。D3 有五個內置偏移量,如圖 5.20 所示。
d3.stackOffsetNone 將所有形狀定位在零基線上方。它是默認偏移量。
以下三個偏移分布基線上方和下方的形狀。d3.stackOffsetDiverging 將正值定位在基線上方,負值定位在基線下方。此偏移最適合堆積條形圖。d3.stackOffsetSilhouette 將基線移動到圖表的中心。d3.stackOffsetWiggle的作用類似,但優化了基線的位置,以最小化擺動或序列的交替上下移動。這三個偏移需要調整垂直刻度的域以適應基線的位置。
最后,d3.stackOffsetExpand 規范化 0 到 1 之間的數據值,使每個索引的總和為 100%。歸一化值時,垂直刻度的域也在 0 和 1 之間變化。
在創建堆疊布局時,我們通常會組合順序和偏移量以達到所需的結果。雖然對于我們何時應該使用順序或偏移量沒有嚴格的規定,但目標應始終是提高可視化的可讀性和/或將注意力集中在我們想要強調的故事上。
對于本章的項目,我們將使用 order() 和 offset() 訪問器函數將堆積面積圖轉換為具有中心基線和堆積條形圖以表示相對值(介于 0 和 100% 之間)的流圖。
在我們開始之前需要注意的一件事是,order() 和 offset() 訪問器函數可以顯著更改注釋數據集中攜帶的值。例如,通過將堆積面積圖轉換為流圖,所表示的銷售價值將不再在 24 到 000,12 之間變化,而是在 -000,12 和 000,3 之間變化。同樣,如果我們使用 d0.stackOffsetExpand 來規范堆疊條形圖顯示的銷售額,則注釋數據將包含在 1 到 <> 之間。在設置垂直刻度的域時,必須考慮這些不同的值。
考慮不同 offset() 訪問器函數帶來的域變化的一種簡單方法是確保我們始終計算注釋數據集中的最小值和最大值,并相應地設置域。
在示例 5.11 中,我們首先聲明兩個空數組,一個存儲每個序列的最小值,另一個存儲最大值。然后我們遍歷帶注釋的數據集,使用 d3.min() 和 d3.max() 找到每個序列的最小值和最大值,并將它們推送到相應的數組中。最后,我們從每個數組中提取最小值和最大值,并使用它們來設置域。
此策略可應用于流圖和堆積條形圖。對于堆積條形圖,您可能希望從比例聲明中刪除 nice() 方法,以僅顯示介于 0 和 1 之間的值。
const minLowerBoundaries=[]; #A
const maxUpperBoundaries=[]; #A
annotatedData.forEach(series=> { #B
minLowerBoundaries.push(d3.min(series, d=> d[0])); #B
maxUpperBoundaries.push(d3.max(series, d=> d[1])); #B
}); #B
const minDomain=d3.min(minLowerBoundaries); #C
const maxDomain=d3.max(maxUpperBoundaries); #C
const yScale=d3.scaleLinear()
.domain([minDomain, maxDomain]) #D
.range([innerHeight, 0])
.nice();
完成此修改后,您可以自由測試偏移值的任何順序,并且 yScale 的域將自動調整。
現在,要將堆疊面積圖轉換為流圖,我們所要做的就是將 order() 和 offset() 訪問器函數鏈接到之前聲明的堆棧生成器。在這里,我們使用訂單 d3.stackOrderInsideOut 與偏移量 d3.stackOffsetSilhouette 結合使用。我們鼓勵您測試一些組合,以了解它們如何影響數據表示。
const stackGenerator=d3.stack()
.keys(formatsInfo.map(f=> f.id))
.order(d3.stackOrderInsideOut)
.offset(d3.stackOffsetSilhouette);
流線圖在美學上令人愉悅,它們肯定會吸引注意力。但它們也更難閱讀。當您想要概述現象隨時間推移的演變時,流圖是一個很好的選擇。但是,如果您希望讀者能夠精確地測量和比較值,堆疊條形圖或成對條形圖是更好的選擇。工具提示還可以幫助提高流圖的可讀性。我們將在第 7 章中構建一個。
同樣,我們通過將其偏移量設置為 d3.stackOffsetExpand 來修改堆積條形圖,這將規范化 0 到 1 之間的銷售值。我們還將順序設置為 d3.stackOrderDescending,以強調 CD 格式在 2000 年左右如何主導市場。再次嘗試一些組合,看看它如何改變圖表傳達的故事焦點。
const stackGenerator=d3.stack()
.keys(formatsInfo.map(f=> f.id))
.order(d3.stackOrderDescending)
.offset(d3.stackOffsetExpand);
在最后一節中,我們將討論如何使用傳統的 HTML 元素輕松構建圖例,并將通過在堆疊條形圖下方放置顏色圖例來將其付諸實踐。圖例是數據可視化的重要組成部分,可幫助讀者解釋他們所看到的內容。
通常,圖例涉及文本,我們知道 SVG 文本并不總是便于操作。如果您查看我們將在圖 5.21 中構建的顏色圖例,您會發現它由一系列彩色方塊和標簽組成,與堆疊條形圖水平居中。使用 SVG 元素構建此圖例將涉及計算每個矩形和文本元素的確切位置。這是可能的,但有一種更簡單的方法。
圖 5.21 我們將在本節中構建的顏色圖例,位于堆積條形圖下方。
D3 不僅用于控制 SVG 元素。它可以創建和操作任何 DOM 元素。這意味著我們可以使用傳統的HTML元素構建圖例,并使用CSS來定位它們。有很多方法可以繼續,但這樣的圖例要求結構化為 HTML 無序列表 ( <ul></ul> )。帶有標簽的每個顏色組合都可以存儲在 <li></li> 元素中,其中一個 <span></span> 元素保存顏色,另一個元素包含標簽,如以下示例所示。
<ul>
<li>
<span> color 1 </span>
<span> label 1 </span>
</li>
<li>
<span> color 2 </span>
<span> label 2 </span>
</li>
...
</ul>
要使用 D3 構建此 HTML 結構,我們轉到文件圖例.js并開始在函數 addLegend() 中工作。在下面的代碼片段中,我們選擇帶有一類 legend-container 的 div,該類已存在于索引中.html .我們將一個 ul 元素附加到這個 div 中,并給它一類顏色圖例。
然后,我們使用數據綁定模式為 formatInfo 數組中包含的每種格式附加一個 li 元素,該數組在共享常量中可用.js .我們將此選擇保存到一個常量中 命名 圖例項 .
我們調用 legendItems 選擇并將 span 元素附加到其中,并根據相關的音樂格式設置跨度的背景顏色屬性。為此,我們可以直接從 格式信息 或調用色標。最后,我們附加另一個 span 元素并將其文本設置為當前格式的標簽鍵。
const legendItems=d3.select(".legend-container")
.append("ul") #A
.attr("class", "color-legend") #A
.selectAll(".color-legend-item") #A
.data(formatsInfo) #A
.join("li") #A
.attr("class", "color-legend-item");
legendItems #B
.append("span") #B
.attr("class", "color-legend-item-color") #B
.style("background-color", d=> d.color); #B
legendItems #C
.append("span") #C
.attr("class", "color-legend-item-label") #C
.text(d=> d.label); #C
如果您應用的類名與上一代碼段中使用的類名相同,則圖例應自動如圖 5.21 所示。這是因為以下樣式已在 base 中設置.css .請注意我們如何使用 CSS flexbox 屬性 (https://css-tricks.com/snippets/css/a-guide-to-flexbox/) 來處理圖例的布局。我們不會花時間解釋這個樣式片段,因為您可能熟悉CSS,這不是本書的重點。這里的主要要點是,有時傳統的HTML元素和CSS樣式比SVG更容易操作,我們可以使用D3來綁定數據和操作任何DOM元素。
.color-legend {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin: 0;
padding-left: 0;
}
.color-legend-item {
margin: 5px 12px;
font-size: 1.4rem;
}
.color-legend span {
display: inline-block;
}
.color-legend-item-color {
position: relative;
top: 2px;
width: 14px;
height: 14px;
margin-right: 5px;
border-radius: 3px;
}
您現在知道如何使用 D3 布局,如餅圖和堆棧布局。在第7章中,我們將把這個項目變成一個交互式可視化。如果您接下來想這樣做,請隨時直接去那里。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。