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
JavaScript 中的變量分為基本類型和引用類型。
如下圖所示:棧內(nèi)存中存放的只是該對(duì)象的訪問(wèn)地址, 在堆內(nèi)存中為這個(gè)值分配空間 。 由于這種值得大小不固定,因此不能把它們保存到棧內(nèi)存中。但內(nèi)存地址大小的固定的,因此可以將內(nèi)存地址保存在棧內(nèi)存中。 這樣,當(dāng)查詢引用類型的變量時(shí), 先從棧中讀取內(nèi)存地址, 然后再通過(guò)地址找到堆中的值。
當(dāng)我們看到一個(gè)變量類型是已知的,就分配在棧里面,比如INT,Double等。其他未知的類型,比如自定義的類型,因?yàn)橄到y(tǒng)不知道需要多大,所以程序自己申請(qǐng),這樣就分配在堆里面。
上方例子得知,當(dāng)我改變arr2中的數(shù)據(jù)時(shí),arr1中數(shù)據(jù)也發(fā)生了變化,當(dāng)改變str1的數(shù)據(jù)值時(shí),arr1卻沒有發(fā)生改變。為什么?這就是傳值與傳址的區(qū)別。
因?yàn)閍rr1是數(shù)組,屬于引用類型,所以它賦予給arr2的時(shí)候傳的是棧中的地址(相當(dāng)于新建了一個(gè)不同名“指針”),而不是堆內(nèi)存中的對(duì)象的值。str1得到的是一個(gè)基本類型的賦值,因此,str1僅僅是從arr1堆內(nèi)存中獲取了一個(gè)數(shù)值,并直接保存在棧中。arr1、arr2都指向同一塊堆內(nèi)存,arr2修改的堆內(nèi)存的時(shí)候,也就會(huì)影響到arr1,str1是直接在棧中修改,并且不能影響到arr1堆內(nèi)存中的數(shù)據(jù)。
基本數(shù)據(jù)類型:基本數(shù)據(jù)類型是指保存在棧內(nèi)存中的簡(jiǎn)單數(shù)據(jù)段。訪問(wèn)方式是按值訪問(wèn)。
var a=1;
a=2 ;
基本類型變量的復(fù)制:從一個(gè)變量向一個(gè)變量復(fù)制時(shí),會(huì)在棧中創(chuàng)建一個(gè)新值,然后把值復(fù)制到為新變量分配的位置上。
var b=a;
vara=newObject();
a.name='xz' ;
為了使程序運(yùn)行時(shí)占用的內(nèi)存最小,通常要實(shí)現(xiàn)垃圾回收機(jī)制。
當(dāng)一個(gè)方法執(zhí)行時(shí),每個(gè)方法都會(huì)建立自己的內(nèi)存棧,在這個(gè)方法內(nèi)定義的變量將會(huì)逐個(gè)放入這塊棧存里,隨著方法的執(zhí)行結(jié)束,這個(gè)方法的棧存也將自然銷毀了。因此,所有在方法中定義的變量都是放在棧內(nèi)存中的;
當(dāng)我們?cè)诔绦蛑袆?chuàng)建一個(gè)對(duì)象時(shí),這個(gè)對(duì)象將被保存到運(yùn)行時(shí)數(shù)據(jù)區(qū)中,以便反復(fù)利用(因?yàn)閷?duì)象的創(chuàng)建成本開銷較大),這個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)就是堆內(nèi)存。堆內(nèi)存中的對(duì)象不會(huì)隨方法的結(jié)束而銷毀,即使方法結(jié)束后,這個(gè)對(duì)象還可能被另一個(gè)引用變量所引用(方法的參數(shù)傳遞時(shí)很常見),則這個(gè)對(duì)象依然不會(huì)被銷毀,只有當(dāng)一個(gè)對(duì)象沒有任何引用變量引用它時(shí),系統(tǒng)的垃圾回收機(jī)制才會(huì)在核實(shí)的時(shí)候回收它。
思考問(wèn)題
demo1.
var a=1;
var b=a;
b=2;
// 這時(shí)a是?
demo1中在變量對(duì)象中的數(shù)據(jù)發(fā)生復(fù)制行為時(shí),系統(tǒng)會(huì)自動(dòng)為新的變量分配一個(gè)新值。var b=a執(zhí)行之后,b雖然重新賦值為2,但是他們其實(shí)已經(jīng)是相互獨(dú)立互不影響的值了。
demo2.
var m={ a: 1, b: 2 }
var n=m;
n.a=2;
// 這時(shí)m.a的值呢?
demo2中我們通過(guò)var n=m執(zhí)行一次復(fù)制引用類型的操作。引用類型的復(fù)制同樣也會(huì)為新的變量自動(dòng)分配一個(gè)新的值保存在變量對(duì)象中,但不同的是,這個(gè)新的值,僅僅只是引用類型的一個(gè)地址指針。當(dāng)?shù)刂分羔樝嗤瑫r(shí),盡管他們相互獨(dú)立,但是在變量對(duì)象中訪問(wèn)到的具體對(duì)象實(shí)際上是同一個(gè)。因此當(dāng)我改變n時(shí),m也發(fā)生了變化。這就是引用類型的特性。
要了解JavaScript數(shù)組的堆棧和隊(duì)列方法的操作,需要先對(duì)堆棧和隊(duì)列基礎(chǔ)知識(shí)有所了解。在繼續(xù)看后面的內(nèi)容之前,我們先簡(jiǎn)單地了解一下堆棧和隊(duì)列的概念。
棧和隊(duì)列都是動(dòng)態(tài)的集合,在棧中,可以去掉的元素是最近插入的那一個(gè)。棧道實(shí)現(xiàn)了后進(jìn)先出。在隊(duì)列中,可以去掉的元素總是在集合中存在的時(shí)間最長(zhǎng)的那一個(gè)。隊(duì)列實(shí)現(xiàn)了先進(jìn)先出的策略。
先上張圖:
棧是一種LIFO(Last-In-First-Out,后進(jìn)先出)的數(shù)據(jù)結(jié)構(gòu),也就是最新添加的項(xiàng)最早被移除。而棧中項(xiàng)的插入(叫做推入)和移除(叫做彈出),只發(fā)生在一個(gè)位置——棧的頂部。
最開始棧中不含有任何數(shù)據(jù),叫做空棧,此時(shí)棧頂就是棧底。然后數(shù)據(jù)從棧頂進(jìn)入,棧頂棧底分離,整個(gè)棧的當(dāng)前容量變大。數(shù)據(jù)出棧時(shí)從棧頂彈出,棧頂下移,整個(gè)棧的當(dāng)前容量變小。
比如說(shuō),我們?cè)谝粋€(gè)箱子中放了很多本書,如果你要拿出第二本書,那么你要先把第一本書拿出來(lái),才能拿第二本書出來(lái);拿出第二本書之后,再把第一本書放進(jìn)去。
ECMAScript為數(shù)組專門提供了 push() 和 pop() 方法,以便實(shí)現(xiàn)類似的行為。 push() 方法可以接收任意數(shù)量的參數(shù),把它們逐個(gè)添加到數(shù)組末尾,并返回修改后數(shù)組的長(zhǎng)度。而 pop() 方法則從數(shù)組末尾移除最后一項(xiàng),減少數(shù)組的length值,然后返回移除的項(xiàng)。
棧數(shù)據(jù)結(jié)構(gòu)的訪問(wèn)規(guī)則是LIFO(后進(jìn)先出),而隊(duì)列數(shù)據(jù)結(jié)構(gòu)的訪問(wèn)規(guī)則是FIFO(Fist-In-First-Out,先進(jìn)先出)。隊(duì)列在列表的末端添加項(xiàng),從列表的前端移除項(xiàng)。如下圖所示:
比如說(shuō)火車站排隊(duì)買票,先到的先買,買好的先走。
入隊(duì)列操作其實(shí)就是在隊(duì)尾追加一個(gè)元素,不需要任何移動(dòng),時(shí)間復(fù)雜度為O(1)。出隊(duì)列則不同,因?yàn)槲覀円呀?jīng)架設(shè)下標(biāo)為0的位置是隊(duì)列的隊(duì)頭,因此每次出隊(duì)列操作所有元素都要向前移動(dòng)。如下圖所示:
ECMAScript為數(shù)組專門提供了 shift() 和 unshift() 方法,以便實(shí)現(xiàn)類似隊(duì)列的行為。由于 push() 是向數(shù)組末端添加數(shù)組項(xiàng)的方法,因此要模擬隊(duì)列只需一個(gè)從數(shù)組前端取得數(shù)組項(xiàng)的方法。實(shí)現(xiàn)這一操作的數(shù)組方法就是 shift() ,它能夠移除數(shù)組中的第一個(gè)項(xiàng)并返回該項(xiàng),同時(shí)將數(shù)組長(zhǎng)度減1。
顧名思義, unshift() 與 shift() 的用途相反:它能在數(shù)組前端添加任意個(gè)數(shù)組項(xiàng)并返回新數(shù)組的長(zhǎng)度。因此,同時(shí)使用 unshift() 和 pop() 方法,可以從相反的方向來(lái)模擬隊(duì)列,即在數(shù)組的前端添加數(shù)組項(xiàng),從數(shù)組末端移除數(shù)組項(xiàng)。
該方法是向數(shù)組末尾添加一個(gè)或者多個(gè)元素,并返回新的長(zhǎng)度。
push()方法可以接收任意數(shù)量的參數(shù),把它們逐個(gè)添加到數(shù)組的末尾,并返回修改后數(shù)組的長(zhǎng)度。如:
var arr=[]; //創(chuàng)建一個(gè)空數(shù)組
console.log(arr); // []
console.log("入棧"); // 入棧
arr.push(1); // 將1添加到數(shù)組arr中
console.log(arr); // [1]
arr.push(2); //將2添加到數(shù)組arr中
console.log(arr); //[1,2]
arr.push([3,4]); // 將數(shù)組[3,4]添加到arr中
console.log(arr); // [1,2,[3,4]]
console.log(arr.length); // 3
在Chrome瀏覽器控制臺(tái)輸出的效果如下圖所示:
pop()方法剛好和push()方法相反。pop()方法刪除數(shù)組的最后一個(gè)元素,把數(shù)組的長(zhǎng)度減1,并且返回它被刪除元素的值,如果數(shù)組變?yōu)榭?,則該方法不改變數(shù)組,返回undefine值。如下代碼演示:
var arr=[1,2,3,4]; //創(chuàng)建一個(gè)數(shù)組
console.log(arr); // [1,2,3,4]
console.log(arr.length); // 4
console.log("出棧,后進(jìn)先出"); // 出棧,后進(jìn)先出
arr.pop();
console.log(arr); // // [1,2,3]
arr.pop();
console.log(arr); // [1,2]
arr.pop();
console.log(arr); // [1]
arr.pop();
console.log(arr); // []
在Chrome瀏覽器控制臺(tái)輸出的效果如下圖所示:
unshift()方法是向數(shù)組的開頭添加一個(gè)或多個(gè)元素,并且返回新的長(zhǎng)度。
var arr=[]; //創(chuàng)建一個(gè)空的數(shù)組
console.log(arr); // []
console.log("入隊(duì)"); // 入隊(duì)
arr.unshift(1,2,3,4); // 將1,2,3,4推入到數(shù)組arr
console.log(arr); // [1,2,3,4]
console.log(arr.length); // 4
在Chrome瀏覽器控制臺(tái)輸出的效果如下圖所示:
shift()方法和unshift()方法恰恰相反。該方法用于把數(shù)組的第一個(gè)元素從其中刪除,并返回被刪除的值。如果數(shù)組是空的,shift()方法將不進(jìn)行任何操作,返回undefined的值。
var arr=[1,2,3,4]; // 創(chuàng)建一個(gè)數(shù)組
console.log(arr); // [1,2,3,4]
arr.shift(); // 取得第一項(xiàng)
console.log(arr); // [2,3,4]
arr.shift(); // 取得第一項(xiàng)
console.log(arr); // [3,4]
arr.shift(); // 取得第一項(xiàng)
console.log(arr); // [4]
arr.shift(); // 取得第一項(xiàng)
console.log(arr); // []
在Chrome瀏覽器控制臺(tái)輸出的效果如下圖所示:
簡(jiǎn)單得回憶一下:
了解這幾種方法之后,我們就可以將它們結(jié)合起來(lái),輕松的實(shí)現(xiàn)類似棧和隊(duì)列的行為。
將push()和pop()結(jié)合在一起,我們就可以實(shí)現(xiàn)類似棧的行為:
//創(chuàng)建一個(gè)數(shù)組來(lái)模擬堆棧
var a=new Array();
console.log(a);
//push: 在數(shù)組的末尾添加一個(gè)或更多元素,并返回新的長(zhǎng)度
console.log("入棧");
a.push(1)
console.log(a);//----->1
a.push(2);
console.log(a);//----->1,2
a.push(3);
console.log(a);//----->1,2,3
a.push(4);
console.log(a);//----->1,2,3,4
console.log("出棧,后進(jìn)先出");
console.log(a);
//pop:從數(shù)組中把最后一個(gè)元素刪除,并返回這個(gè)元素的值
a.pop();//----->4
console.log(a);
a.pop();//----->3
console.log(a);
a.pop();//----->2
console.log(a);
a.pop();//----->1
console.log(a);
在Chrome瀏覽器控制臺(tái)輸出的效果如下圖所示:
將shift()和push()方法結(jié)合在一起,可以像使用隊(duì)列一樣使用數(shù)組。即在數(shù)組的后端添加項(xiàng),從數(shù)組的前端移除項(xiàng):
//創(chuàng)建一個(gè)數(shù)組來(lái)模擬隊(duì)列
//創(chuàng)建一個(gè)數(shù)組來(lái)模擬隊(duì)列
var a=new Array();
console.log(a);
//push: 在數(shù)組的末尾添加一個(gè)或更多元素,并返回新的長(zhǎng)度
console.log("入隊(duì)");
a.push(1)
console.log(a);//----->1
a.push(2);
console.log(a);//----->1,2
a.push(3);
console.log(a);//----->1,2,3
a.push(4);
console.log(a);//----->1,2,3,4
console.log("出隊(duì),先進(jìn)先出");
console.log(a);
//shift:從數(shù)組中把第一個(gè)元素刪除,并返回這個(gè)元素的值
a.shift();//----->1
console.log(a);
a.shift();//----->2
console.log(a);
a.shift();//----->3
console.log(a);
a.shift();//----->4
console.log(a);
在Chrome瀏覽器控制臺(tái)輸出的效果如下圖所示:
除此之外,還可以同時(shí)使用unshift()和pop()方法,從相反的方向來(lái)模擬隊(duì)列,即在數(shù)組的前端添加項(xiàng),從數(shù)組的后端移除項(xiàng)。如下面的示例所示:
//創(chuàng)建一個(gè)數(shù)組來(lái)模擬隊(duì)列
//創(chuàng)建一個(gè)數(shù)組來(lái)模擬隊(duì)列
var a=new Array();
console.log(a);
//unshift: 在數(shù)組的前端添加一個(gè)或更多元素,并返回新的長(zhǎng)度
console.log("入隊(duì)");
a.unshift(1)
console.log(a);//----->1
a.unshift(2);
console.log(a);//----->2,1
a.unshift(3);
console.log(a);//----->3,2,1
a.unshift(4);
console.log(a);//----->4,3,2,1
console.log("出隊(duì),先進(jìn)先出");
console.log(a);
//pop:從數(shù)組中把最一個(gè)元素刪除,并返回這個(gè)元素的值
a.pop();//----->4
console.log(a);
a.pop();//----->3
console.log(a);
a.pop();//----->2
console.log(a);
a.pop();//----->1
console.log(a);
在Chrome瀏覽器控制臺(tái)輸出的效果如下圖所示:
Array的push()與unshift()方法都能給當(dāng)前數(shù)組添加元素,不同的是,push()是在末尾添加,而unshift()則是在開頭添加,從原理就可以知道,unshift()的效率是較低的。原因是,它每添加一個(gè)元素,都要把現(xiàn)有元素往下移一個(gè)位置。但到底效率差異有多大呢?下面來(lái)簡(jiǎn)單測(cè)試一下。
/*
關(guān)于代碼中"var s=+newDate();"的技巧說(shuō)明
解釋如下:=+這個(gè)運(yùn)算符是不存在的;
+相當(dāng)于.valueOf();
+new Date()相當(dāng)于new Date().valueOf()
//4個(gè)結(jié)果一樣返回當(dāng)前時(shí)間的毫秒數(shù)
alert(+new Date());
alert(+new Date);
var s=new Date();
alert(s.valueOf());
alert(s.getTime());
*/
var arr=[ ];
var startTime=+new Date(); //+new Date()相當(dāng)于new Date().valueOf(),返回當(dāng)前時(shí)間的毫秒數(shù)
// push性能測(cè)試
for (var i=0; i < 100000; i++) {
arr.push(i);
}
var endTime=+new Date();
console.log("調(diào)用push方法往數(shù)組中添加100000個(gè)元素耗時(shí)"+(endTime-startTime)+"毫秒");
startTime=+new Date();
arr=[ ];
// unshift性能測(cè)試
for (var i=0; i < 100000; i++) {
arr.unshift(i);
}
endTime=+new Date();
console.log("調(diào)用unshift方法往數(shù)組中添加100000個(gè)元素耗時(shí)"+(endTime-startTime)+"毫秒");
這段代碼分別執(zhí)行了100000次push()和unshift()操作,在chrome瀏覽器運(yùn)行一次,得到的結(jié)果如下圖所示:
可見,unshift()比push()要慢差不多100倍!因此,平時(shí)還是要慎用unshift(),特別是對(duì)大數(shù)組。那如果一定要達(dá)到unshift()的效果,可以借助于Array的reverse()方法,Array的reverse()的方法能夠把一個(gè)數(shù)組反轉(zhuǎn)。先把要放進(jìn)數(shù)組的元素用push()添加,再執(zhí)行一次reverse(),就達(dá)到了unshift()的效果。比如:
//創(chuàng)建一個(gè)數(shù)組來(lái)模擬堆棧
var a=new Array();
//使用push方法在數(shù)組的末尾添加元素
a.push(1)
a.push(2);
a.push(3);
a.push(4);
console.log("數(shù)組反轉(zhuǎn)之前數(shù)組中的元素順序");
console.log(a);//----->1,2,3,4
//Array有一個(gè)叫做reverse的方法,能夠把一個(gè)數(shù)組反轉(zhuǎn)。先把要放進(jìn)數(shù)組的元素用push添加,再執(zhí)行一次reverse,就達(dá)到了unshift的效果
a.reverse();//使用reverse方法將數(shù)組進(jìn)行反轉(zhuǎn)
console.log("數(shù)組反轉(zhuǎn)之后數(shù)組中的元素順序");
console.log(a);
在chrome瀏覽器控制臺(tái)輸出的效果如下圖所示:
從運(yùn)行結(jié)果來(lái)看,數(shù)組元素的順序已經(jīng)反轉(zhuǎn)過(guò)來(lái)了。
reverse()方法的性能測(cè)試
var arr=[ ], s=+new Date;
for (var i=0; i < 100000; i++) {
arr.push(i);
}
//調(diào)用reverse方法將數(shù)組里面的100000元素的順序反轉(zhuǎn)
arr.reverse();
console.log("調(diào)用reverse方法將數(shù)組里面的100000元素的順序反轉(zhuǎn)耗時(shí):"+(+new Date - s)+"毫秒");
在chrome瀏覽器控制臺(tái)輸出的效果如下圖所示:
從運(yùn)行效果中可以看到,reverse()方法的性能極高,可以放心使用。
本文主要介紹了JavaScript數(shù)組的push()、pop()、shift()和unshift()方法。并且如何通過(guò)組合這幾種方法實(shí)現(xiàn)類似棧和隊(duì)例的行為。
js中刪除堆棧:
1:js中的splice方法
splice(index,len,[item]) 注釋:該方法會(huì)改變?cè)紨?shù)組。
splice有3個(gè)參數(shù),它也可以用來(lái)替換/刪除/添加數(shù)組內(nèi)某一個(gè)或者幾個(gè)值
index:數(shù)組開始下標(biāo) len: 替換/刪除的長(zhǎng)度 item:替換的值,刪除操作的話 item為空
如:arr=['a','b','c','d']
刪除 ---- item不設(shè)置
arr.splice(1,1) //['a','c','d'] 刪除起始下標(biāo)為1,長(zhǎng)度為1的一個(gè)值,len設(shè)置的1,如果為0,則數(shù)組不變
arr.splice(1,2) //['a','d'] 刪除起始下標(biāo)為1,長(zhǎng)度為2的一個(gè)值,len設(shè)置的2
替換 ---- item為替換的值
arr.splice(1,1,'ttt') //['a','ttt','c','d'] 替換起始下標(biāo)為1,長(zhǎng)度為1的一個(gè)值為‘ttt’,len設(shè)置的1
arr.splice(1,2,'ttt') //['a','ttt','d'] 替換起始下標(biāo)為1,長(zhǎng)度為2的兩個(gè)值為‘ttt’,len設(shè)置的1
添加 ---- len設(shè)置為0,item為添加的值
arr.splice(1,0,'ttt') //['a','ttt','b','c','d'] 表示在下標(biāo)為1處添加一項(xiàng)‘ttt’
看來(lái)還是splice最方便啦
2:delete delete刪除掉數(shù)組中的元素后,會(huì)把該下標(biāo)出的值置為undefined,數(shù)組的長(zhǎng)度不會(huì)變
如:delete arr[1] //['a', ,'c','d'] 中間出現(xiàn)兩個(gè)逗號(hào),數(shù)組長(zhǎng)度不變,有一項(xiàng)為undefined
好地了解數(shù)據(jù)結(jié)構(gòu)如何工作
這聽起來(lái)是否熟悉:"我通過(guò)完成網(wǎng)上課程開始了前端開發(fā)"
您可能正在尋求提高計(jì)算機(jī)科學(xué)的基礎(chǔ)知識(shí),尤其是在數(shù)據(jù)結(jié)構(gòu)和算法方面。 今天,我們將介紹一些常見的數(shù)據(jù)結(jié)構(gòu),并以JavaScript實(shí)施它們。
希望這部分內(nèi)容可以補(bǔ)充您的技能!
堆棧遵循LIFO(后進(jìn)先出)的原理。 如果您堆疊書籍,則最上層的書籍將排在最底層的書籍之前。 或者,當(dāng)您在Internet上瀏覽時(shí),后退按鈕會(huì)將您帶到最近瀏覽的頁(yè)面。
Stack具有以下常見方法:
· push:輸入一個(gè)新元素
· pop:刪除頂部元素,返回刪除的元素
· peek:返回頂部元素
· length:返回堆棧中的元素?cái)?shù)
Javascript中的數(shù)組具有Stack的屬性,但是我們使用Stack()函數(shù)從頭開始構(gòu)建Stack
function Stack() { this.count=0; this.storage={}; this.push=function (value) { this.storage[this.count]=value; this.count++; } this.pop=function () { if (this.count===0) { return undefined; } this.count--; var result=this.storage[this.count]; delete this.storage[this.count]; return result; } this.peek=function () { return this.storage[this.count - 1]; } this.size=function () { return this.count; } }
隊(duì)列類似于堆棧。 唯一的區(qū)別是Queue使用FIFO原理(先進(jìn)先出)。 換句話說(shuō),當(dāng)您排隊(duì)等候總線時(shí),隊(duì)列中的第一個(gè)將始終排在第一位。
隊(duì)列具有以下方法:
· enqueue 入隊(duì):輸入隊(duì)列,在最后添加一個(gè)元素
· dequeue 出隊(duì):離開隊(duì)列,移除前元素并返回
· front:獲取第一個(gè)元素
· isEmpty:確定隊(duì)列是否為空
· size:獲取隊(duì)列中的元素?cái)?shù))
JavaScript中的數(shù)組具有Queue的某些屬性,因此我們可以使用數(shù)組來(lái)構(gòu)造Queue的示例:
function Queue() { var collection=[]; this.print=function () { console.log(collection); } this.enqueue=function (element) { collection.push(element); } this.dequeue=function () { return collection.shift(); } this.front=function () { return collection[0]; } this.isEmpty=function () { return collection.length===0; } this.size=function () { return collection.length; } }
優(yōu)先隊(duì)列
隊(duì)列還有另一個(gè)高級(jí)版本。 為每個(gè)元素分配優(yōu)先級(jí),并將根據(jù)優(yōu)先級(jí)對(duì)它們進(jìn)行排序:
function PriorityQueue() { ... this.enqueue=function (element) { if (this.isEmpty()) { collection.push(element); } else var added=false; for (var i=0; i < collection.length; i++) { if (element[1] < collection[i][1]) { collection.splice(i, 0, element); added=true; break; } } if (!added) { collection.push(element); } } } }
測(cè)試一下:
var pQ=new PriorityQueue(); pQ.enqueue([ gannicus , 3]); pQ.enqueue([ spartacus , 1]); pQ.enqueue([ crixus , 2]); pQ.enqueue([ oenomaus , 4]); pQ.print();
結(jié)果:
[ [ spartacus , 1 ], [ crixus , 2 ], [ gannicus , 3 ], [ oenomaus , 4 ] ]
從字面上看,鏈表是一個(gè)鏈?zhǔn)綌?shù)據(jù)結(jié)構(gòu),每個(gè)節(jié)點(diǎn)由兩部分信息組成:該節(jié)點(diǎn)的數(shù)據(jù)和指向下一個(gè)節(jié)點(diǎn)的指針。 鏈表和常規(guī)數(shù)組都是帶有序列化存儲(chǔ)的線性數(shù)據(jù)結(jié)構(gòu)。 當(dāng)然,它們也有差異:
單邊鏈表通常具有以下方法:
· size:返回節(jié)點(diǎn)數(shù)
· head:返回head的元素
· add:在尾部添加另一個(gè)節(jié)點(diǎn)
· delete:刪除某些節(jié)點(diǎn)
· indexOf:返回節(jié)點(diǎn)的索引
· elementAt:返回索引的節(jié)點(diǎn)
· addAt:在特定索引處插入節(jié)點(diǎn)
· removeAt:刪除特定索引處的節(jié)點(diǎn)
/** Node in the linked list **/ function Node(element) { // Data in the node this.element=element; // Pointer to the next node this.next=null; } function LinkedList() { var length=0; var head=null; this.size=function () { return length; } this.head=function () { return head; } this.add=function (element) { var node=new Node(element); if (head==null) { head=node; } else { var currentNode=head; while (currentNode.next) { currentNode=currentNode.next; } currentNode.next=node; } length++; } this.remove=function (element) { var currentNode=head; var previousNode; if (currentNode.element===element) { head=currentNode.next; } else { while (currentNode.element !==element) { previousNode=currentNode; currentNode=currentNode.next; } previousNode.next=currentNode.next; } length--; } this.isEmpty=function () { return length===0; } this.indexOf=function (element) { var currentNode=head; var index=-1; while (currentNode) { index++; if (currentNode.element===element) { return index; } currentNode=currentNode.next; } return -1; } this.elementAt=function (index) { var currentNode=head; var count=0; while (count < index) { count++; currentNode=currentNode.next; } return currentNode.element; } this.addAt=function (index, element) { var node=new Node(element); var currentNode=head; var previousNode; var currentIndex=0; if (index > length) { return false; } if (index===0) { node.next=currentNode; head=node; } else { while (currentIndex < index) { currentIndex++; previousNode=currentNode; currentNode=currentNode.next; } node.next=currentNode; previousNode.next=node; } length++; } this.removeAt=function (index) { var currentNode=head; var previousNode; var currentIndex=0; if (index < 0 || index >=length) { return null; } if (index===0) { head=currentIndex.next; } else { while (currentIndex < index) { currentIndex++; previousNode=currentNode; currentNode=currentNode.next; } previousNode.next=currentNode.next; } length--; return currentNode.element; } }
集合是數(shù)學(xué)的基本概念:定義明確且不同的對(duì)象的集合。 ES6引入了集合的概念,它與數(shù)組有一定程度的相似性。 但是,集合不允許重復(fù)元素,也不會(huì)被索引。
一個(gè)典型的集合具有以下方法:
· values:返回集合中的所有元素
· size:返回元素?cái)?shù)
· has:確定元素是否存在
· add:將元素插入集合
· delete:從集合中刪除元素
· union:返回兩組的交集
· difference:返回兩組的差異
· subset:確定某個(gè)集合是否是另一個(gè)集合的子集
為了區(qū)分ES6中的集合,在以下示例中我們聲明為MySet:
function MySet() { var collection=[]; this.has=function (element) { return (collection.indexOf(element) !==-1); } this.values=function () { return collection; } this.size=function () { return collection.length; } this.add=function (element) { if (!this.has(element)) { collection.push(element); return true; } return false; } this.remove=function (element) { if (this.has(element)) { index=collection.indexOf(element); collection.splice(index, 1); return true; } return false; } this.union=function (otherSet) { var unionSet=new MySet(); var firstSet=this.values(); var secondSet=otherSet.values(); firstSet.forEach(function (e) { unionSet.add(e); }); secondSet.forEach(function (e) { unionSet.add(e); }); return unionSet; } this.intersection=function (otherSet) { var intersectionSet=new MySet(); var firstSet=this.values(); firstSet.forEach(function (e) { if (otherSet.has(e)) { intersectionSet.add(e); } }); return intersectionSet; } this.difference=function (otherSet) { var differenceSet=new MySet(); var firstSet=this.values(); firstSet.forEach(function (e) { if (!otherSet.has(e)) { differenceSet.add(e); } }); return differenceSet; } this.subset=function (otherSet) { var firstSet=this.values(); return firstSet.every(function (value) { return otherSet.has(value); }); } }
哈希表是鍵值數(shù)據(jù)結(jié)構(gòu)。 由于通過(guò)鍵查詢值的閃電般的速度,它通常用于Map,Dictionary或Object數(shù)據(jù)結(jié)構(gòu)中。 如上圖所示,哈希表使用哈希函數(shù)將鍵轉(zhuǎn)換為數(shù)字列表,這些數(shù)字用作相應(yīng)鍵的值。 要快速使用鍵獲取價(jià)值,時(shí)間復(fù)雜度可以達(dá)到O(1)。 相同的鍵必須返回相同的值-這是哈希函數(shù)的基礎(chǔ)。
哈希表具有以下方法:
· add:添加鍵值對(duì)
· delete:刪除鍵值對(duì)
· find:使用鍵查找對(duì)應(yīng)的值
Java簡(jiǎn)化哈希表的示例:
function hash(string, max) { var hash=0; for (var i=0; i < string.length; i++) { hash +=string.charCodeAt(i); } return hash % max; } function HashTable() { let storage=[]; const storageLimit=4; this.add=function (key, value) { var index=hash(key, storageLimit); if (storage[index]===undefined) { storage[index]=[ [key, value] ]; } else { var inserted=false; for (var i=0; i < storage[index].length; i++) { if (storage[index][i][0]===key) { storage[index][i][1]=value; inserted=true; } } if (inserted===false) { storage[index].push([key, value]); } } } this.remove=function (key) { var index=hash(key, storageLimit); if (storage[index].length===1 && storage[index][0][0]===key) { delete storage[index]; } else { for (var i=0; i < storage[index]; i++) { if (storage[index][i][0]===key) { delete storage[index][i]; } } } } this.lookup=function (key) { var index=hash(key, storageLimit); if (storage[index]===undefined) { return undefined; } else { for (var i=0; i < storage[index].length; i++) { if (storage[index][i][0]===key) { return storage[index][i][1]; } } } } }
樹數(shù)據(jù)結(jié)構(gòu)是多層結(jié)構(gòu)。 與Array,Stack和Queue相比,它也是一種非線性數(shù)據(jù)結(jié)構(gòu)。 在插入和搜索操作期間,此結(jié)構(gòu)非常高效。 讓我們看一下樹數(shù)據(jù)結(jié)構(gòu)的一些概念:
· root:樹的根節(jié)點(diǎn),無(wú)父節(jié)點(diǎn)
· parent 父節(jié)點(diǎn):上層的直接節(jié)點(diǎn),只有一個(gè)
· children 子節(jié)點(diǎn):較低層的直接節(jié)點(diǎn),可以有多個(gè)
· siblings 兄弟姐妹:共享同一父節(jié)點(diǎn)
· leaf 葉:沒有子節(jié)點(diǎn)
· edge 邊緣:節(jié)點(diǎn)之間的分支或鏈接
· path 路徑:從起始節(jié)點(diǎn)到目標(biāo)節(jié)點(diǎn)的邊緣
· height of node 節(jié)點(diǎn)高度:特定節(jié)點(diǎn)到葉節(jié)點(diǎn)的最長(zhǎng)路徑的邊數(shù)
· height of tree 樹的高度:根節(jié)點(diǎn)到葉節(jié)點(diǎn)的最長(zhǎng)路徑的邊數(shù)
· depth of node 節(jié)點(diǎn)深度:從根節(jié)點(diǎn)到特定節(jié)點(diǎn)的邊數(shù)
· degree of node 節(jié)點(diǎn)度:子節(jié)點(diǎn)數(shù)
這是二叉搜索樹的示例。 每個(gè)節(jié)點(diǎn)最多有兩個(gè)節(jié)點(diǎn),左節(jié)點(diǎn)小于當(dāng)前節(jié)點(diǎn),右節(jié)點(diǎn)大于當(dāng)前節(jié)點(diǎn):
二進(jìn)制搜索樹中的常用方法:
· add:在樹中插入一個(gè)節(jié)點(diǎn)
· findMin:獲取最小節(jié)點(diǎn)
· findMax:獲取最大節(jié)點(diǎn)
· find:搜索特定節(jié)點(diǎn)
· isPresent:確定某個(gè)節(jié)點(diǎn)的存在
· delete:從樹中刪除節(jié)點(diǎn)
JavaScript中的示例:
class Node { constructor(data, left=null, right=null) { this.data=data; this.left=left; this.right=right; } } class BST { constructor() { this.root=null; } add(data) { const node=this.root; if (node===null) { this.root=new Node(data); return; } else { const searchTree=function (node) { if (data < node.data) { if (node.left===null) { node.left=new Node(data); return; } else if (node.left !==null) { return searchTree(node.left); } } else if (data > node.data) { if (node.right===null) { node.right=new Node(data); return; } else if (node.right !==null) { return searchTree(node.right); } } else { return null; } }; return searchTree(node); } } findMin() { let current=this.root; while (current.left !==null) { current=current.left; } return current.data; } findMax() { let current=this.root; while (current.right !==null) { current=current.right; } return current.data; } find(data) { let current=this.root; while (current.data !==data) { if (data < current.data) { current=current.left } else { current=current.right; } if (current===null) { return null; } } return current; } isPresent(data) { let current=this.root; while (current) { if (data===current.data) { return true; } if (data < current.data) { current=current.left; } else { current=current.right; } } return false; } remove(data) { const removeNode=function (node, data) { if (node==null) { return null; } if (data==node.data) { // no child node if (node.left==null && node.right==null) { return null; } // no left node if (node.left==null) { return node.right; } // no right node if (node.right==null) { return node.left; } // has 2 child nodes var tempNode=node.right; while (tempNode.left !==null) { tempNode=tempNode.left; } node.data=tempNode.data; node.right=removeNode(node.right, tempNode.data); return node; } else if (data < node.data) { node.left=removeNode(node.left, data); return node; } else { node.right=removeNode(node.right, data); return node; } } this.root=removeNode(this.root, data); } }
測(cè)試一下:
const bst=new BST(); bst.add(4); bst.add(2); bst.add(6); bst.add(1); bst.add(3); bst.add(5); bst.add(7); bst.remove(4); console.log(bst.findMin()); console.log(bst.findMax()); bst.remove(7); console.log(bst.findMax()); console.log(bst.isPresent(4));
結(jié)果:
1 7 6 false
Trie或"前綴樹"也是一種搜索樹。 Trie分步存儲(chǔ)數(shù)據(jù)-樹中的每個(gè)節(jié)點(diǎn)代表一個(gè)步驟。 Trie用于存儲(chǔ)詞匯,因此可以快速搜索,尤其是自動(dòng)完成功能。
Trie中的每個(gè)節(jié)點(diǎn)都有一個(gè)字母-在分支之后可以形成一個(gè)完整的單詞。 它還包含一個(gè)布爾指示符,以顯示這是否是最后一個(gè)字母。
Trie具有以下方法:
· add:在字典樹中插入一個(gè)單詞
· isWord:確定樹是否由某些單詞組成
· print:返回樹中的所有單詞
/** Node in Trie **/ function Node() { this.keys=new Map(); this.end=false; this.setEnd=function () { this.end=true; }; this.isEnd=function () { return this.end; } } function Trie() { this.root=new Node(); this.add=function (input, node=this.root) { if (input.length===0) { node.setEnd(); return; } else if (!node.keys.has(input[0])) { node.keys.set(input[0], new Node()); return this.add(input.substr(1), node.keys.get(input[0])); } else { return this.add(input.substr(1), node.keys.get(input[0])); } } this.isWord=function (word) { let node=this.root; while (word.length > 1) { if (!node.keys.has(word[0])) { return false; } else { node=node.keys.get(word[0]); word=word.substr(1); } } return (node.keys.has(word) && node.keys.get(word).isEnd()) ? true : false; } this.print=function () { let words=new Array(); let search=function (node=this.root, string) { if (node.keys.size !=0) { for (let letter of node.keys.keys()) { search(node.keys.get(letter), string.concat(letter)); } if (node.isEnd()) { words.push(string); } } else { string.length > 0 ? words.push(string) : undefined; return; } }; search(this.root, new String()); return words.length > 0 ? words : null; } }
圖(有時(shí)稱為網(wǎng)絡(luò))是指具有鏈接(或邊)的節(jié)點(diǎn)集。 根據(jù)鏈接是否具有方向,它可以進(jìn)一步分為兩組(即有向圖和無(wú)向圖)。 Graph在我們的生活中得到了廣泛使用,例如,在導(dǎo)航應(yīng)用中計(jì)算最佳路線,或者在社交媒體中向推薦的朋友舉兩個(gè)例子。
圖有兩種表示形式:
鄰接表
在此方法中,我們?cè)谧髠?cè)列出所有可能的節(jié)點(diǎn),并在右側(cè)顯示已連接的節(jié)點(diǎn)。
鄰接矩陣
鄰接矩陣顯示行和列中的節(jié)點(diǎn),行和列的交點(diǎn)解釋節(jié)點(diǎn)之間的關(guān)系,0表示未鏈接,1表示鏈接,> 1表示不同的權(quán)重。
要查詢圖中的節(jié)點(diǎn),必須使用"廣度優(yōu)先"(BFS)方法或"深度優(yōu)先"(DFS)方法在整個(gè)樹形網(wǎng)絡(luò)中進(jìn)行搜索。
讓我們看一個(gè)用Javascript編寫B(tài)FS的示例:
function bfs(graph, root) { var nodesLen={}; for (var i=0; i < graph.length; i++) { nodesLen[i]=Infinity; } nodesLen[root]=0; var queue=[root]; var current; while (queue.length !=0) { current=queue.shift(); var curConnected=graph[current]; var neighborIdx=[]; var idx=curConnected.indexOf(1); while (idx !=-1) { neighborIdx.push(idx); idx=curConnected.indexOf(1, idx + 1); } for (var j=0; j < neighborIdx.length; j++) { if (nodesLen[neighborIdx[j]]==Infinity) { nodesLen[neighborIdx[j]]=nodesLen[current] + 1; queue.push(neighborIdx[j]); } } } return nodesLen; }
測(cè)試一下:
var graph=[ [0, 1, 1, 1, 0], [0, 0, 1, 0, 0], [1, 1, 0, 0, 0], [0, 0, 0, 1, 0], [0, 1, 0, 0, 0] ]; console.log(bfs(graph, 1));
結(jié)果:
{ 0: 2, 1: 0, 2: 1, 3: 3, 4: Infinity }
就是這樣–我們涵蓋了所有常見的數(shù)據(jù)結(jié)構(gòu),并提供了JavaScript中的示例。 這應(yīng)該使您更好地了解計(jì)算機(jī)中數(shù)據(jù)結(jié)構(gòu)的工作方式。 編碼愉快!
(本文翻譯自Kingsley Tan的文章《8 Common Data Structures in Javascript》, 參考 https://medium.com/better-programming/8-common-data-structures-in-javascript-3d3537e69a27)
在上一章中,我們討論了 D3 如何使用其形狀生成器函數(shù)計(jì)算復(fù)雜形狀(如曲線、面積和弧)的 d 屬性。在本章中,我們將通過(guò)布局將這些形狀提升到另一個(gè)層次。在 D3 中,布局是將數(shù)據(jù)集作為輸入并生成新的批注數(shù)據(jù)集作為輸出的函數(shù),其中包含繪制特定可視化效果所需的屬性。例如,餅圖布局計(jì)算餅圖每個(gè)扇區(qū)的角度,并使用這些角度批注數(shù)據(jù)集。同樣,堆棧布局計(jì)算堆積形狀在堆積條形圖或流圖中的位置。
布局不會(huì)繪制可視化效果,也不會(huì)像組件一樣調(diào)用它們,也不會(huì)像形狀生成器那樣在繪圖代碼中引用。相反,它們是一個(gè)預(yù)處理步驟,用于設(shè)置數(shù)據(jù)的格式,以便準(zhǔn)備好以您選擇的形式顯示。
圖5.1 布局功能是用于計(jì)算繪制特定圖表所需信息的數(shù)據(jù)預(yù)處理步驟。
在本章中,我們將餅圖和堆棧布局與第 4 章中討論的弧形和面積形狀生成器相結(jié)合,以創(chuàng)建圖 5.2 所示的項(xiàng)目。您也可以在 https://d3js-in-action-third-edition.github.io/visualizing-40-years-of-music-industry-sales/ 在線找到它。該項(xiàng)目可視化了 1973 年至 2019 年間音樂行業(yè)每種格式的銷售情況。它的靈感來(lái)自2020年MakeoverMonday(www.makeovermonday.co.uk/week-21-2020/)舉辦的挑戰(zhàn)。
圖 5.2 1973 年至 2019 年音樂行業(yè)銷售的可視化。這是我們將在本章中構(gòu)建的項(xiàng)目。
雖然本章只介紹了餅圖和堆棧布局,但其他布局,如和弦布局和更奇特的布局,遵循相同的原則,看完這些應(yīng)該很容易理解。
在開始之前,請(qǐng)轉(zhuǎn)到第 5 章的代碼文件。您可以從本書的 Github 存儲(chǔ)庫(kù)下載它們(https://github.com/d3js-in-action-third-edition/code-files)。在名為 chapter_05 的文件夾中,代碼文件按節(jié)進(jìn)行組織。要開始本章的練習(xí),請(qǐng)?jiān)诖a編輯器中打開 5.1-Pie_layout/start 文件夾并啟動(dòng)本地 Web 服務(wù)器。如果您需要有關(guān)設(shè)置本地開發(fā)環(huán)境的幫助,請(qǐng)參閱附錄 A。您可以在位于本章代碼文件根目錄下的自述文件中找到有關(guān)項(xiàng)目文件夾結(jié)構(gòu)的更多詳細(xì)信息。
我們將在本章中構(gòu)建的三個(gè)可視化(圓環(huán)圖、堆積條形圖和流圖)共享相同的數(shù)據(jù)、維度和比例。為了避免重復(fù),該項(xiàng)目被分解為多個(gè) JavaScript 文件,其中一個(gè)用于可視化共享的常量,另一個(gè)專門用于比例。這種方法將使我們的代碼更易于閱讀和修改。在生產(chǎn)代碼中,我們可能會(huì)使用 JavaScript 導(dǎo)入和導(dǎo)出來(lái)訪問(wèn)不同的函數(shù),并結(jié)合 Node 和捆綁器。在討論前端框架時(shí),我們將到達(dá)那里,但現(xiàn)在,我們將堅(jiān)持一個(gè)類似遺留的項(xiàng)目結(jié)構(gòu),以保持對(duì) D3 的關(guān)注。請(qǐng)注意,D3 庫(kù)和所有 JavaScript 文件都已加載到 index.html 中。
使用本章的代碼文件時(shí),在代碼編輯器中僅打開一個(gè)開始文件夾或一個(gè)結(jié)束文件夾。如果一次打開章節(jié)的所有文件并使用 Live Server 擴(kuò)展為項(xiàng)目提供服務(wù),則數(shù)據(jù)文件的路徑將無(wú)法按預(yù)期工作。
在本節(jié)中,我們將使用 D3 的餅圖布局來(lái)創(chuàng)建圓環(huán)圖,您可以在圖 5.2 頂部和托管項(xiàng)目 (https://d3js-in-action-third-edition.github.io/visualizing-40-years-of-music-industry-sales/) 上看到該圓環(huán)圖。更具體地說(shuō),我們將可視化 1975 年、1995 年和 2013 年每種音樂格式的銷售額細(xì)分。每個(gè)圓環(huán)圖的中心將對(duì)應(yīng)于相應(yīng)年份在流圖和下面堆疊條形圖的 x 軸上的位置。
讓我們花點(diǎn)時(shí)間建立一個(gè)策略,以確保每個(gè)圖表根據(jù) x 軸上的年份正確水平對(duì)齊。一個(gè)簡(jiǎn)單的方法是使用第4章中描述的保證金約定。隨著本章的進(jìn)展,我們將使用三個(gè) SVG 容器:一個(gè)用于圓環(huán)圖,一個(gè)用于流圖,一個(gè)用于堆積條形圖。這些容器中的每一個(gè)都具有相同的尺寸并共享相同的邊距。為內(nèi)部圖表保留的區(qū)域(沒有軸和標(biāo)簽的可視化效果)也將具有相同的維度并水平對(duì)齊,如圖 5.3 所示。文件 js/shared-constant.js 已包含可視化共享的邊距對(duì)象和維度常量。
我們還在 js/load-data 中為您加載了 CSV 數(shù)據(jù)文件.js .有關(guān)如何將數(shù)據(jù)加載到 D3 項(xiàng)目中的更多信息,請(qǐng)參閱第 4 章和第 3 章。加載數(shù)據(jù)后,我們調(diào)用函數(shù) defineScales() 和 drawDonutCharts() ,我們將在本節(jié)中使用它們。
首先,讓我們?yōu)閳A環(huán)圖追加一個(gè) SVG 容器和一個(gè)定義為內(nèi)部圖表保留區(qū)域的 SVG 組。為此,我們轉(zhuǎn)到 js/donut-charts.js并在函數(shù) drawDonutCharts() 中,我們創(chuàng)建 SVG 容器和一個(gè) SVG 組。在下面的代碼片段中,您將看到我們?cè)?div 內(nèi)附加了 SVG 容器,ID 為 donut 。請(qǐng)注意,我們通過(guò)根據(jù)圖表的左邊距和上邊距平移組來(lái)應(yīng)用邊距約定。
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
您可能想知道為什么我們需要將邊距約定應(yīng)用于圓環(huán)圖,因?yàn)闆]有軸和標(biāo)簽的帳戶空間。這是因?yàn)槊總€(gè)圓環(huán)圖將根據(jù)其所代表的年份水平定位。由于我們希望這些年份的水平位置與下面的流線圖和堆疊條形圖中相同,因此我們需要考慮邊際慣例。
在第 4 章中,我們討論了極坐標(biāo)以及如何通過(guò)將弧包含在 SVG 組中并將該組轉(zhuǎn)換為圖表中心的位置來(lái)促進(jìn)餅圖或圓環(huán)圖的創(chuàng)建。通過(guò)以這種方式進(jìn)行,弧線將自動(dòng)圍繞該中心繪制。
我們將在這里應(yīng)用相同的策略,唯一的區(qū)別是我們需要考慮三個(gè)圓環(huán)圖,并且它們的中心水平位置對(duì)應(yīng)于它們所代表的年份,如圖 5.4 所示。
圖 5.4 組成圓環(huán)圖的每組弧都包含在 SVG 組中。這些組根據(jù)它們所代表的年份進(jìn)行水平翻譯。該位置是使用 D3 刻度計(jì)算的。
要計(jì)算每個(gè)甜甜圈中心的水平位置,我們需要一個(gè)刻度。如您所知,我們使用 D3 刻度將數(shù)據(jù)(此處為年份)轉(zhuǎn)換為屏幕屬性,此處為水平位置。線性或時(shí)間刻度對(duì)于我們的目的來(lái)說(shuō)效果很好,但我們選擇波段刻度,因?yàn)槲覀冎牢覀兩院髸?huì)繪制一個(gè)堆疊條形圖,它將共享相同的刻度。有關(guān)頻段刻度工作原理的更多說(shuō)明,請(qǐng)參閱第 3 章。
在文件中 js/scale.js ,我們首先使用函數(shù) d3.scaleBand() 初始化波段刻度,并將其存儲(chǔ)在名為 xScale 的常量中。請(qǐng)注意我們?nèi)绾卧诤瘮?shù) defineScales() 中聲明刻度的域和范圍。這種方法讓我們等到數(shù)據(jù)加載完成,然后再嘗試使用它來(lái)設(shè)置域(一旦數(shù)據(jù)準(zhǔn)備就緒,函數(shù) defineScales() 從加載數(shù)據(jù)調(diào)用.js)。我們?cè)诤瘮?shù)外部聲明常量 xScale,使其可以從其他 js 文件訪問(wèn)。
const xScale=d3.scaleBand(); #A
const defineScales=(data)=> {
xScale
.domain(data.map(d=> d.year)) #B
.range([0, innerWidth]); #B
};
帶狀刻度接受離散輸入作為域,并返回該范圍的連續(xù)輸出。在清單 5.1 中,我們使用 JavaScript map() 方法,通過(guò)每年從數(shù)據(jù)集創(chuàng)建一個(gè)數(shù)組來(lái)設(shè)置域。對(duì)于范圍,我們傳遞一個(gè)數(shù)組,其中包含可用水平空間的最小值(零)和最大值(對(duì)應(yīng)于內(nèi)部圖表的 innerWidth)。
我們回到函數(shù) drawDonutCharts() ,正如你在清單 5.2 中看到的,我們首先聲明一個(gè)名為 years 的數(shù)組,它列出了我們感興趣的年份,這里是 1975、1995 和 2013。然后,使用 forEach() 循環(huán),我們?yōu)楦信d趣的每一年附加一個(gè) SVG 組,并將其保存在名為 donutContainer 的常量中。最后,我們通過(guò)設(shè)置組的轉(zhuǎn)換屬性來(lái)翻譯組。水平平移是通過(guò)調(diào)用計(jì)算的 xScale ,我們將當(dāng)前年份傳遞到該平移,而垂直平移對(duì)應(yīng)于內(nèi)部圖表的半高。
const years=[1975, 1995, 2013];
years.forEach(year=> {
const donutContainer=donutContainers
.append("g")
.attr("transform", `translate(${xScale(year)}, ${innerHeight/2})`);
});
完成準(zhǔn)備步驟后,我們現(xiàn)在可以專注于圓環(huán)圖。餅圖和圓環(huán)圖可視化部分與整體的關(guān)系或每個(gè)扇區(qū)相對(duì)于總量表示的數(shù)量。D3 餅圖布局生成器通過(guò)根據(jù)每個(gè)切片所代表的百分比計(jì)算每個(gè)切片的開始和結(jié)束角度來(lái)幫助我們。
D3 的餅圖生成器希望輸入數(shù)據(jù)格式化為數(shù)字?jǐn)?shù)組。例如,對(duì)于 1975 年,我們可以有一個(gè)數(shù)組,其中包含與每種音樂格式對(duì)應(yīng)的銷售額,如下所示:
const sales1975=[8061.8, 2770.4, 469.5, 0, 0, 0, 48.5];
雖然這樣一個(gè)簡(jiǎn)單的數(shù)組足以生成餅圖,但它會(huì)阻止我們以后根據(jù)它所代表的音樂格式為每個(gè)切片分配顏色。為了隨身攜帶這些信息,我們可以使用一個(gè)對(duì)象數(shù)組,其中包含音樂格式的 ID 和感興趣年份的相關(guān)銷售額。
在示例 5.3 中,我們首先從加載數(shù)據(jù)集的 columns 屬性中提取格式。獲取數(shù)據(jù)時(shí),例如,使用 d3.csv() 方法,D3 將一個(gè)數(shù)組附加到數(shù)據(jù)集,其中包含原始 CSV 數(shù)據(jù)集中每列的標(biāo)題,并使用鍵 data.columns 進(jìn)行訪問(wèn)。如果將提取的數(shù)據(jù)記錄到控制臺(tái)中,則會(huì)在數(shù)據(jù)數(shù)組的末尾看到它,如圖 5.5 所示。
由于我們只對(duì)音樂格式感興趣,因此我們可以過(guò)濾列數(shù)組以刪除“year”標(biāo)簽。
圖 5.5 從 CSV 文件獲取數(shù)據(jù)時(shí),D3 將數(shù)組附加到數(shù)據(jù)集,其中包含原始數(shù)據(jù)集中列的標(biāo)題??梢允褂面I data.columns 訪問(wèn)此數(shù)組。
為了準(zhǔn)備餅圖生成器的數(shù)據(jù),我們還需要提取感興趣的年份的數(shù)據(jù)。我們使用 JavaScript 方法 find() 隔離這些數(shù)據(jù),并將其存儲(chǔ)在名為 yearData 的常量中。
我們遍歷格式數(shù)組,對(duì)于每種格式,我們創(chuàng)建一個(gè)對(duì)象,其中包含格式 id 及其感興趣年份的相關(guān)銷售額。最后,我們將這個(gè)對(duì)象推入 數(shù)組格式化數(shù)據(jù) ,之前聲明。
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
});
準(zhǔn)備就緒后,格式化數(shù)據(jù)是一個(gè)對(duì)象數(shù)組,每個(gè)對(duì)象都包含格式的 id 及其感興趣年份的相關(guān)銷售額。
//=> 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 }
];
現(xiàn)在數(shù)據(jù)格式正確,我們可以初始化餅圖布局生成器。我們用方法 d3.pie() 構(gòu)造一個(gè)新的餅圖生成器,它是 d3 形狀模塊 (https://github.com/d3/d3-shape#pies) 的一部分。由于格式化數(shù)據(jù)是一個(gè)對(duì)象數(shù)組,我們需要告訴餅圖生成器哪個(gè)鍵包含將決定切片大小的值。我們通過(guò)設(shè)置 value() 訪問(wèn)器函數(shù)來(lái)做到這一點(diǎn),如以下代碼片段所示。我們還將 pie 生成器存儲(chǔ)在一個(gè)名為 pieGenerator 的常量中,以便我們可以像調(diào)用任何其他函數(shù)一樣調(diào)用它。
const pieGenerator=d3.pie()
.value(d=> d.sales);
要生成餅圖布局的數(shù)據(jù),我們只需調(diào)用餅圖生成器函數(shù),將格式化的數(shù)據(jù)作為參數(shù)傳遞,并將結(jié)果存儲(chǔ)在名為 注釋數(shù)據(jù) .
const pieGenerator=d3.pie()
.value(d=> d.sales);
const annotatedData=pieGenerator(formattedData);
餅圖生成器返回一個(gè)新的帶批注的數(shù)據(jù)集,其中包含對(duì)原始數(shù)據(jù)集的引用,但也包括新屬性:每個(gè)切片的值、其索引及其開始和結(jié)束角度(以弧度為單位)。請(qǐng)注意,每個(gè)切片之間的填充也包括 padAngle 并且當(dāng)前設(shè)置為零。我們稍后會(huì)改變這一點(diǎn)。
//=> annotatedData=[
{
data: { format: "vinyl", sales: 8061.8 },
value: 8061.8,
index: 0,
startAngle: 0,
endAngle: 4.5,
padAngle: 0,
},
...
];
請(qǐng)務(wù)必了解餅圖布局生成器不直接參與繪制餅圖。這是一個(gè)預(yù)處理步驟,用于計(jì)算餅圖扇區(qū)的角度。如圖5.1和5.6所述,此過(guò)程通常包括三個(gè)步驟:
圖 5.6 餅圖布局生成器是一個(gè)預(yù)處理步驟,用于生成一個(gè)帶注釋的數(shù)據(jù)集,其中包含餅圖每個(gè)切片的開始和結(jié)束角度。該過(guò)程通常涉及格式化我們的數(shù)據(jù),初始化餅圖生成器函數(shù),并調(diào)用該函數(shù)以獲取帶注釋的數(shù)據(jù)。
準(zhǔn)備好帶注釋的數(shù)據(jù)集后,是時(shí)候生成弧線了!您將看到以下步驟與上一章中創(chuàng)建弧的方式非常相似。出于這個(gè)原因,我們不會(huì)解釋每一個(gè)細(xì)節(jié)。如果您需要更深入的討論,請(qǐng)參閱第 4 章。
在示例 5.4 中,我們首先通過(guò)調(diào)用 d3.arc() 方法及其負(fù)責(zé)設(shè)置圖表內(nèi)外半徑、切片之間的填充以及切片角半徑的各種訪問(wèn)器函數(shù)來(lái)初始化 arc 生成器。如果內(nèi)半徑設(shè)置為零,我們將獲得一個(gè)餅圖,而如果它大于零,我們將得到一個(gè)圓環(huán)圖。
與第 4 章中使用的策略的唯一區(qū)別是,這次我們可以在聲明電弧發(fā)生器的同時(shí)設(shè)置 startAngle() 和 endAngle() 訪問(wèn)器函數(shù)。這是因?yàn)楝F(xiàn)在,這些值包含在帶注釋的數(shù)據(jù)集中,我們可以告訴這些訪問(wèn)器函數(shù)如何通過(guò) d.startAngle 和 d.endAngle .
要使弧出現(xiàn)在屏幕上,我們需要做的最后一件事是使用數(shù)據(jù)綁定模式為注釋數(shù)據(jù)集中的每個(gè)對(duì)象生成一個(gè)路徑元素(每個(gè)弧或切片都有一個(gè)對(duì)象)。請(qǐng)注意,在清單 5.4 中,我們?nèi)绾螢槊總€(gè)甜甜圈的弧指定一個(gè)特定的類名 ( 'arc-${year}' ),并將該類名用作數(shù)據(jù)綁定模式中的選擇器。由于我們正在循環(huán)中創(chuàng)建甜甜圈,這將防止 D3 在制作新甜甜圈時(shí)覆蓋每個(gè)甜甜圈。
最后,我們調(diào)用弧發(fā)生器函數(shù)來(lái)計(jì)算每條路徑的 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
如果您保存項(xiàng)目并在瀏覽器中查看圓環(huán)圖,您會(huì)發(fā)現(xiàn)它們的形狀是正確的,但每個(gè)弧線都是漆黑的。這是正常的,黑色是 SVG 路徑的默認(rèn)填充屬性。為了提高可讀性,我們將根據(jù)每個(gè)弧線所代表的音樂格式對(duì)它們應(yīng)用不同的顏色。
將正確的顏色應(yīng)用于每個(gè)弧的一種簡(jiǎn)單且可重用的方法是聲明色階。在 D3 中,色階通常使用 d3.scaleOrdinal() (https://github.com/d3/d3-scale#scaleOrdinal) 創(chuàng)建。序數(shù)刻度將離散域映射到離散范圍。在我們的例子中,域是音樂格式的數(shù)組,范圍是包含與每種格式關(guān)聯(lián)的顏色的數(shù)組。
在文件比例中.js ,我們首先聲明一個(gè)序數(shù)比例并將其保存在常量色階中。然后,我們通過(guò)將 formatInfo 數(shù)組(在共享常量中可用.js的每個(gè)格式 id 映射到數(shù)組中來(lái)設(shè)置其域。我們對(duì)顏色做同樣的事情,您可以根據(jù)自己的喜好進(jìn)行個(gè)性化設(shè)置。在本章中,我們將重用此色階來(lái)創(chuàng)建構(gòu)成我們項(xiàng)目的所有圖表。
const colorScale=d3.scaleOrdinal();
const defineScales=(data)=> {
colorScale
.domain(formatsInfo.map(f=> f.id))
.range(formatsInfo.map(f=> f.color));
};
回到圓環(huán)圖.js我們可以通過(guò)將綁定到每個(gè)弧的音樂格式 id 傳遞給色階來(lái)設(shè)置弧的填充屬性。
const arcs=donutContainer
.selectAll(`.arc-${year}`)
.data(annotatedData)
.join("path")
.attr("class", `arc-${year}`)
.attr("d", arcGenerator)
.attr("fill", d=> colorScale(d.data.format));
保存您的項(xiàng)目并在瀏覽器中查看。看起來(lái)還不錯(cuò)!弧線已按降序顯示,從最大到最小,這有助于提高可讀性。我們已經(jīng)可以看到音樂的面貌在 1975 年、1995 年和 2013 年間發(fā)生了怎樣的變化,主導(dǎo)格式完全不同。
圖 5.7 1975年、1995年和2013年的圓環(huán)圖
在第4章中,我們提到餅圖有時(shí)很難解釋,因?yàn)槿四X不太擅長(zhǎng)將角度轉(zhuǎn)換為比率。我們可以通過(guò)在圓環(huán)圖的質(zhì)心上添加一個(gè)標(biāo)簽來(lái)提高圓環(huán)圖的可讀性,該標(biāo)簽以百分比表示每個(gè)弧的值,就像我們?cè)谏弦徽轮兴龅哪菢印?/span>
在示例 5.5 中,我們稍微修改了用于創(chuàng)建圓弧的代碼(來(lái)自示例 5.4)。首先,我們使用數(shù)據(jù)綁定模式來(lái)追加 SVG 組而不是路徑元素。然后,我們將路徑元素(用于圓弧)和 SVG 文本元素(用于標(biāo)簽)附加到這些組中。由于父母將綁定數(shù)據(jù)傳遞給孩子,因此我們將在塑造弧線和標(biāo)簽時(shí)訪問(wèn)數(shù)據(jù)。
我們通過(guò)調(diào)用電弧發(fā)生器來(lái)繪制電弧,就像我們之前所做的那樣。要設(shè)置標(biāo)簽的文本,我們需要計(jì)算每個(gè)弧線表示的比率或百分比。我們通過(guò)從弧的結(jié)束角度中減去弧的起始角并將結(jié)果除以 2π(以弧度為單位的完整圓覆蓋的角度)來(lái)執(zhí)行此計(jì)算。請(qǐng)注意我們?nèi)绾问褂美ㄌ?hào)表示法(d[“百分比”])將百分比值存儲(chǔ)到綁定數(shù)據(jù)中。當(dāng)我們需要對(duì)不同的屬性進(jìn)行相同的計(jì)算時(shí),這個(gè)技巧很有用。它可以防止您多次重復(fù)計(jì)算。為了返回標(biāo)簽的文本,我們將計(jì)算出的百分比傳遞給方法 d3.format(“.0%”) ,該方法生成一個(gè)舍入百分比并在標(biāo)簽?zāi)┪蔡砑右粋€(gè)百分比符號(hào)。
我們應(yīng)用相同的策略來(lái)計(jì)算每個(gè)弧的質(zhì)心,這是我們想要放置標(biāo)簽的位置。當(dāng)設(shè)置標(biāo)簽的 x 屬性時(shí),我們計(jì)算相關(guān)弧的質(zhì)心(使用第 4 章中討論的技術(shù))并將其存儲(chǔ)在綁定數(shù)據(jù)中( d[“質(zhì)心”])。然后,在設(shè)置 y 屬性時(shí),質(zhì)心數(shù)組已經(jīng)可以通過(guò) d.centroid 訪問(wèn)。
為了使標(biāo)簽水平和垂直地以質(zhì)心居中,我們需要將其文本錨點(diǎn)和主要基線屬性設(shè)置為中間。我們還使用fill屬性將它們的顏色設(shè)置為白色,將其字體大小增加到16px,將其字體粗細(xì)增加到500以提高可讀性。
如果您保存項(xiàng)目并在瀏覽器中查看圓環(huán)圖,您會(huì)發(fā)現(xiàn)標(biāo)簽在大弧上工作良好,但在較小的弧線上幾乎無(wú)法讀取。在專業(yè)項(xiàng)目中,我們可以通過(guò)將小弧的標(biāo)簽移動(dòng)到圓環(huán)圖之外來(lái)解決這個(gè)問(wèn)題。對(duì)于此項(xiàng)目,當(dāng)百分比小于 5% 時(shí),我們根本不會(huì)通過(guò)將其填充不透明度屬性設(shè)置為零來(lái)顯示這些標(biāo)簽。
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 帶百分比標(biāo)簽的圓環(huán)圖
作為最后一步,我們將指示圓環(huán)圖所代表的年份,標(biāo)簽位于其中心。我們通過(guò)在每個(gè)甜甜圈容器中附加一個(gè)文本元素來(lái)做到這一點(diǎn)。因?yàn)槲覀冞€在循環(huán)往復(fù)年份,所以我們可以直接應(yīng)用當(dāng)前年份作為標(biāo)簽的文本。此外,由于圓環(huán)容器位于圖表的中心,因此文本元素會(huì)自動(dòng)正確定位。我們所要做的就是設(shè)置其文本錨點(diǎn)和主要基線屬性,使其水平和垂直居中。
donutContainer
.append("text")
.text(year)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.style("font-size", "24px")
.style("font-weight", 500);
瞧!我們的圓環(huán)圖是完整的。
圖 5.9 帶有年份標(biāo)簽的完整圓環(huán)圖
圖 5.10 回顧了創(chuàng)建餅圖或圓環(huán)圖的步驟。在第一步中,我們使用布局函數(shù) d3.pie() 預(yù)處理數(shù)據(jù),以獲得帶有注釋的數(shù)據(jù)集,其中包含每個(gè)切片的角度。然后,我們使用弧發(fā)生器函數(shù)繪制弧線,該函數(shù)從注釋數(shù)據(jù)集中獲取角度并返回每個(gè)路徑的 d 屬性。最后,我們使用 SVG 文本元素添加標(biāo)簽以提高圖表的可讀性。
圖 5.10 創(chuàng)建餅圖或圓環(huán)圖所涉及的主要步驟。
到目前為止,我們已經(jīng)處理了可以在任何傳統(tǒng)電子表格中輕松創(chuàng)建的信息可視化的簡(jiǎn)單示例。但是你進(jìn)入這個(gè)行業(yè)并不是為了制作類似Excel的圖表。您可能希望用漂亮的數(shù)據(jù)讓您的觀眾驚嘆不已,為您的美學(xué) je ne sais quoi 贏得獎(jiǎng)項(xiàng),并通過(guò)您隨著時(shí)間的推移而變化的表現(xiàn)喚起深刻的情感反應(yīng)。
流圖是代表變化和變化的崇高信息可視化。在你開始把這些部分放在一起之前,創(chuàng)作似乎具有挑戰(zhàn)性。歸根結(jié)底,流圖是所謂的堆積面積圖的變體。這些層相互累積,并根據(jù)靠近中心的組件所占用的空間來(lái)調(diào)整上方和下方元素的面積。它似乎是有機(jī)的,因?yàn)檫@種吸積性模仿了許多生物的生長(zhǎng)方式,似乎暗示了控制生物生長(zhǎng)和衰敗的各種涌現(xiàn)特性。我們稍后會(huì)解釋它的外觀,但首先,讓我們弄清楚如何構(gòu)建它。
我們?cè)诒緯牡谝徊糠挚戳艘粋€(gè)流線圖,因?yàn)樗鼘?shí)際上并沒有那么奇特。流圖是一種堆積圖,這意味著它與堆積條形圖基本相似,如圖 5.11 所示。流線圖也類似于我們?cè)谏弦徽轮袠?gòu)建的折線圖后面的區(qū)域,只是這些區(qū)域相互堆疊。在本節(jié)中,我們將使用 D3 的堆棧和面積生成器來(lái)創(chuàng)建堆疊條形圖,然后創(chuàng)建流線圖。
圖 5.11 流圖與堆積條形圖基本相似。在 D3 中,兩者都是使用堆棧布局生成器創(chuàng)建的。
在 D3 中,創(chuàng)建堆積條形圖或流圖的步驟類似,如圖 5.12 所示。首先,我們初始化一個(gè)堆棧布局生成器并設(shè)置堆棧的參數(shù)。然后,我們將原始數(shù)據(jù)集傳遞給堆棧生成器,堆棧生成器將返回一個(gè)新的注釋數(shù)據(jù)集,指示每個(gè)數(shù)據(jù)點(diǎn)的下限和上限。如果我們制作一個(gè)流線圖,我們還必須初始化一個(gè)面積生成器,類似于上一章中討論的直線和曲線生成器。最后,我們將帶注釋的數(shù)據(jù)集綁定到制作圖表所需的 SVG 形狀、堆疊條形圖的矩形或流圖的路徑。在流圖的情況下,調(diào)用面積生成器來(lái)計(jì)算路徑的 d 屬性。我們將在以下小節(jié)中更詳細(xì)地介紹這些步驟。
圖 5.12 使用 D3 創(chuàng)建堆積圖的步驟。
堆棧布局生成器是一個(gè) D3 函數(shù),它將具有多個(gè)類別的數(shù)據(jù)集作為輸入。本章示例中使用的數(shù)據(jù)集包含 1973 年至 2019 年間每年不同音樂格式的總銷售額。每種音樂格式將成為堆疊圖表中的一個(gè)系列。
與前面討論的餅圖布局生成器一樣,堆棧布局函數(shù)返回一個(gè)新的注釋數(shù)據(jù)集,其中包含不同序列在“堆疊”到另一個(gè)時(shí)的位置。堆棧生成器是 d3 形狀模塊 (https://github.com/d3/d3-shape#stacks) 的一部分。
讓我們將堆棧布局付諸行動(dòng),并開始在位于堆疊條形圖中的函數(shù) drawStackedBars() 中工作.js 。請(qǐng)注意,此函數(shù)已經(jīng)包含將 SVG 容器附加到 div 的代碼,ID 為 “bars”,以及內(nèi)部圖表的組容器。這與我們?cè)诘?章中使用的策略相同,與保證金慣例并行。
在下面的代碼片段中,我們首先使用方法 d3.stack() 聲明一個(gè)堆棧生成器,并將其存儲(chǔ)在一個(gè)名為 stackGenerator 的常量中。然后,我們需要告訴生成器數(shù)據(jù)集中的哪些鍵包含我們要堆疊的值(將成為序列)。我們使用 keys() 訪問(wèn)器函數(shù)來(lái)做到這一點(diǎn),我們將類別 id 數(shù)組傳遞給該函數(shù),這里是每種音樂格式的標(biāo)識(shí)符。我們通過(guò)映射 formatInfo 常量的 id 來(lái)創(chuàng)建這個(gè)數(shù)組。我們還可以使用附加到數(shù)據(jù)集的列鍵并過(guò)濾掉年份,就像我們?cè)?5.1.2 節(jié)中所做的那樣。
最后,我們調(diào)用堆棧生成器并將數(shù)據(jù)作為參數(shù)傳遞,以獲得帶注釋的數(shù)據(jù)集。我們將新數(shù)據(jù)集存儲(chǔ)在名為 注釋數(shù)據(jù) .
const stackGenerator=d3.stack() #A
.keys(formatsInfo.map(f=> f.id)); #B
const annotatedData=stackGenerator(data); #C
如果將帶注釋的數(shù)據(jù)集記錄到控制臺(tái)中,您將看到它由多維數(shù)組組成。我們首先為每個(gè)系列提供一個(gè)數(shù)組,如圖 5.13 所示,序列的 id 可通過(guò) key 屬性獲得。然后,序列數(shù)組包含另一組數(shù)組,數(shù)據(jù)集中每年一個(gè)數(shù)組。最后這些數(shù)組包括相關(guān)年份類別的下限和上限以及該年份的原始數(shù)據(jù)。下限和上限分別由索引 d[0] 和 d[1] 訪問(wèn),如果 d 對(duì)應(yīng)于數(shù)組。
格式“乙烯基”是堆棧布局處理的第一個(gè)鍵。請(qǐng)注意,它的下限始終為零,而其上邊界對(duì)應(yīng)于該格式的當(dāng)年銷售額。然后,以下類別是“8 軌”。8 軌的下邊界對(duì)應(yīng)于黑膠唱片的上邊界,我們將 8 軌的銷量相加以獲得其上限,從而創(chuàng)建一個(gè)堆棧。
圖 5.13 堆棧布局生成器返回的帶注釋的數(shù)據(jù)集。
如果“堆棧”的概念還不清楚,下圖可能會(huì)有所幫助。如果我們從原始數(shù)據(jù)集中仔細(xì)觀察 1986 年,我們將看到音樂主要通過(guò)三種格式提供:黑膠唱片的銷量為 2,825M$,盒式磁帶為 5,830M$,CD 為 2,170M$。我們?cè)趫D5.14的左側(cè)顯示了這些數(shù)據(jù)點(diǎn),獨(dú)立繪制。
當(dāng)我們使用堆棧布局時(shí),我們創(chuàng)建所謂的“數(shù)據(jù)列”而不是“數(shù)據(jù)點(diǎn)”,每列都有下限和上限。如果我們的堆棧從黑膠唱片開始,則下限為零,上邊界對(duì)應(yīng)于 1986 年黑膠唱片的銷售額:2,825M$。然后,我們將盒式磁帶的銷售疊加在其上:下邊界對(duì)應(yīng)于黑膠唱片的上限(2,825M$),上邊界是黑膠唱片和盒式磁帶(8,655M$)的銷售量。這個(gè)上邊界成為CD銷售的下限,其上邊界對(duì)應(yīng)于三種格式(10,825M$)的銷售量相加。這些邊界在帶注釋的數(shù)據(jù)集中通過(guò)索引(d[0]和d[1])訪問(wèn)。
圖 5.14 堆棧布局生成器將數(shù)據(jù)點(diǎn)轉(zhuǎn)換為堆疊數(shù)據(jù)列,并返回包含每個(gè)數(shù)據(jù)列的下限和上限的帶注釋的數(shù)據(jù)集。在這里,我們看到 1986 年的一個(gè)例子。
在本節(jié)中,我們將創(chuàng)建您在圖 5.11 底部看到的堆積條形圖。堆積條形圖類似于我們?cè)诘?2 章和第 3 章中已經(jīng)制作的條形圖,只是條形圖分為多個(gè)類別或系列。堆積條形圖和一般的堆積可視化通常用于顯示趨勢(shì)隨時(shí)間推移的演變。
就像我們對(duì)圓環(huán)圖所做的那樣,我們將使用堆棧布局返回的帶注釋的數(shù)據(jù)集來(lái)繪制對(duì)應(yīng)于每個(gè)類別的條形。但首先,我們需要一個(gè)垂直軸的比例,將每個(gè)矩形的下邊界和上邊界轉(zhuǎn)換為垂直位置。我們希望條形的高度與銷售額成線性比例,因此我們將使用線性刻度。由于此刻度需要訪問(wèn)帶注釋的數(shù)據(jù),因此我們將在函數(shù) drawStackedBars() 中聲明它。
刻度域從零到注釋數(shù)據(jù)中可用的最大上限。我們知道,這個(gè)最大值必須存在于最后一個(gè)帶注釋的數(shù)據(jù)系列中,這些數(shù)據(jù)將位于圖表的頂部。我們可以使用 length 屬性訪問(wèn)這個(gè)系列( annotatedData[annotatedData.length - 1])。然后,我們使用方法 d3.max() 檢索屬性 d[1] 下的最大值,該值對(duì)應(yīng)于上限。
垂直刻度的范圍從內(nèi)部圖表底部的innerHeight到內(nèi)部圖表頂部的零(請(qǐng)記住,SVG垂直軸在向下方向上為正)。最后,我們將 scale 聲明與方法 .nice() 鏈接起來(lái),這將確保域以“nice”舍入值結(jié)束,而不是注釋數(shù)據(jù)集中的實(shí)際最大值。
const maxUpperBoundary=d3.max(annotatedData[annotatedData.length - 1], d
?=> d[1]);
const yScale=d3.scaleLinear()
.domain([0, maxUpperBoundary])
.range([innerHeight, 0])
.nice();
我們現(xiàn)在已準(zhǔn)備好附加條形圖。為此,我們遍歷帶注釋的數(shù)據(jù),并逐個(gè)附加序列,如清單 5.7 中所述。我們從數(shù)據(jù)綁定模式開始,為系列數(shù)組中的每個(gè)項(xiàng)目或年份附加一個(gè)矩形元素(每種音樂格式都有一個(gè)系列)。請(qǐng)注意我們?nèi)绾螌⑴c當(dāng)前系列相關(guān)的類名應(yīng)用于矩形并將其用作選擇器。如果我們簡(jiǎn)單地使用“rect”元素作為選擇器,每次執(zhí)行循環(huán)時(shí),先前創(chuàng)建的矩形都將被刪除并替換為新矩形。
然后,我們通過(guò)調(diào)用帶刻度的帶寬屬性來(lái)設(shè)置矩形的 x 屬性,通過(guò)將當(dāng)前年份傳遞給 xScale 來(lái)設(shè)置它們的寬度屬性。y 屬性對(duì)應(yīng)于矩形左上角的垂直位置,由前面聲明的垂直刻度返回,我們將矩形的上邊界 (d[1] ) 傳遞到該刻度。
同樣,矩形的高度是其上邊界和下邊界位置之間的差異。這里有一點(diǎn)問(wèn)題。因?yàn)?SVG 垂直軸在向下方向上是正的,所以 yScale(d[0]) 返回的值高于 yScale(d[1])。我們需要從前者中減去后者,以避免為 y 屬性提供負(fù)值,這會(huì)引發(fā)錯(cuò)誤。
最后,我們通過(guò)將當(dāng)前音樂格式傳遞給色階來(lái)設(shè)置 fill 屬性,該色階可在每個(gè)系列的 key 屬性下訪問(wèn),如前面圖 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
});
如果保存項(xiàng)目,您將看到條形之間沒有水平空間。我們可以通過(guò)回到 xScale 的聲明來(lái)解決這個(gè)問(wèn)題,并將其 paddingInner() 訪問(wèn)器函數(shù)設(shè)置為值 20%,就像我們?cè)诘?3 章中所做的那樣。
xScale
.domain(data.map(d=> d.year))
.range([0, innerWidth])
.paddingInner(0.2);
為了完成我們的堆積條形圖,我們需要添加軸。在清單 5.9 中,我們首先使用方法 d3.axisBottom() 聲明一個(gè)底部軸,并將 xScale 作為引用傳遞。
我們將軸聲明與方法鏈接起來(lái), .tickValues() ,它允許我們陳述我們希望在圖表上看到的確切刻度和標(biāo)簽。否則,D3 將每年提供一對(duì)刻度和標(biāo)簽,看起來(lái)會(huì)很局促且難以閱讀。方法.tickValues()將值數(shù)組作為參數(shù)。我們使用方法 d3.range() 生成這個(gè)數(shù)組,并聲明我們想要從 1975 年到 2020 年的每個(gè)整數(shù),步長(zhǎng)為 5。
我們還使用方法 .tickSizeOuter() 隱藏底部軸兩端的刻度,我們向其傳遞值為零。方法tickValues()和tickSizeOuter()都可以在d3軸模塊(https://github.com/d3/d3-axis)中找到,而d3.range()是d3-array模塊(https://github.com/d3/d3-array)的一部分。
最后,我們使用 call() 方法將底部軸附加到圖表中,在轉(zhuǎn)換為底部的組中,并對(duì)左軸執(zhí)行相同的操作。
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
如果保存項(xiàng)目并在瀏覽器中查看它,您可能會(huì)發(fā)現(xiàn)軸標(biāo)簽有點(diǎn)太小。此外,如第 4 章所述,D3 將字體族“sans-serif”應(yīng)用于包含軸元素的 SVG 組,這意味著項(xiàng)目的字體系列不會(huì)被繼承。從 CSS 文件可視化中.css ,我們可以使用選擇器 .tick 文本定位軸標(biāo)簽并修改其樣式屬性。在下面的代碼片段中,我們更改了它們的字體系列、字體大小和字體粗細(xì)屬性。
.tick text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: 500;
}
完成后,堆積條形圖將類似于圖 5.15 中的條形圖,但看起來(lái)還不像圖 5.2 中的條形圖或托管項(xiàng)目 (https://d3js-in-action-third-edition.github.io/visualizing-40-years-of-music-industry-sales/) 中的條形圖。我們一會(huì)兒就到那里。
圖5.15 第一版堆積條形圖
在上一小節(jié)中,我們使用堆棧布局函數(shù)生成一個(gè)帶注釋的數(shù)據(jù)集,從中繪制堆疊條形圖的矩形?,F(xiàn)在,我們將應(yīng)用類似的策略來(lái)繪制流圖。盡管流圖看起來(lái)比堆積條形圖更復(fù)雜,但它們很容易在 D3 中創(chuàng)建。主要區(qū)別在于,對(duì)于流圖,我們使用帶注釋的數(shù)據(jù)集來(lái)追加區(qū)域,而為堆疊條形圖附加矩形。
在本小節(jié)中,我們將使用函數(shù) drawStreamGraph() ,您可以在文件流圖中找到它.js 。此函數(shù)已包含將 SVG 容器附加到 div 的代碼,ID 為 “streamgraph”,以及內(nèi)部圖表的組容器。這與我們?cè)诘?章中使用的策略相同,與保證金慣例并行。
在示例 5.10 中,我們初始化堆棧生成器并調(diào)用它來(lái)獲取帶注釋的數(shù)據(jù)。我們還聲明了一個(gè)線性刻度來(lái)計(jì)算垂直邊界的位置。這與我們用于堆積條形圖的代碼完全相同?,F(xiàn)在,不要擔(dān)心我們正在復(fù)制代碼。我們將在下一小節(jié)中回到它。
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
為了繪制堆疊區(qū)域,我們需要一個(gè)區(qū)域生成器函數(shù),該函數(shù)將負(fù)責(zé)計(jì)算用于繪制序列的每個(gè)路徑元素的 d 屬性。如第4章所述,面積生成器至少使用三個(gè)訪問(wèn)器函數(shù),在我們的例子中,一個(gè)用于檢索每個(gè)數(shù)據(jù)點(diǎn)的水平位置,一個(gè)用于堆疊區(qū)域的下邊界,另一個(gè)用于它們的上邊界。圖 5.16 說(shuō)明了面積生成器如何應(yīng)用于堆疊區(qū)域。
圖 5.16 面積生成器 d3.area() 與三個(gè)或更多訪問(wèn)器函數(shù)組合在一起。當(dāng)與流圖的堆棧布局結(jié)合使用時(shí),它使用每個(gè)數(shù)據(jù)點(diǎn)的下限和上限(y0 和 y1)來(lái)計(jì)算區(qū)域的 d 屬性。
在下面的代碼片段中,我們初始化了區(qū)域生成器 d3.area() 。首先,我們使用 x() 訪問(wèn)器函數(shù)來(lái)計(jì)算每個(gè)數(shù)據(jù)點(diǎn)的水平位置。由于 xScale 是波段刻度,因此它返回相關(guān)年份的每個(gè)波段開頭的位置,該位置可在每個(gè)數(shù)據(jù)點(diǎn)的數(shù)據(jù)對(duì)象中的注釋數(shù)據(jù)集中訪問(wèn) ( d.data.year)。如果我們希望數(shù)據(jù)點(diǎn)與下面堆疊條形圖的條形中心水平對(duì)齊,我們需要將數(shù)據(jù)點(diǎn)向右平移,寬度為條形寬度的一半,我們可以用帶寬()屬性計(jì)算帶刻度。
然后,我們使用 y0() 和 y(1) 訪問(wèn)器函數(shù)來(lái)確定數(shù)據(jù)點(diǎn)沿每個(gè)序列的下邊界和上邊界的垂直位置。這個(gè)位置是用 yScale 計(jì)算的,之前聲明了,我們將邊界的值傳遞給邊界值,可以通過(guò)邊界數(shù)據(jù)中的數(shù)組索引訪問(wèn):d[0] 表示下邊界,d[1] 表示上限邊界。
最后,如果我們想沿每個(gè)邊界插值數(shù)據(jù)點(diǎn)以獲得曲線而不是直線,我們使用 curve() 訪問(wèn)器函數(shù)。這里我們選擇了曲線插值函數(shù)d3.curveCatmullRom。如前所述,曲線插值會(huì)修改數(shù)據(jù)的表示,必須謹(jǐn)慎選擇。有關(guān)討論和演示,請(qǐng)參閱第 4.2.2 節(jié)。
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);
現(xiàn)在,我們已準(zhǔn)備好繪制堆疊區(qū)域!首先,我們使用數(shù)據(jù)綁定模式為注釋數(shù)據(jù)集中的每個(gè)序列生成一個(gè) SVG 路徑元素。我們調(diào)用面積生成器函數(shù)來(lái)獲取每個(gè)路徑的 d 屬性,以及它們的填充屬性的色階。
請(qǐng)注意我們?nèi)绾卧?SVG 組中附加路徑以保持標(biāo)記井井有條且易于檢查。這也將有助于在以后保持區(qū)域和垂直網(wǎng)格的適當(dāng)并置。
innerChart
.append("g")
.attr("class", "areas-container")
.selectAll("path")
.data(annotatedData)
.join("path")
.attr("d", areaGenerator)
.attr("fill", d=> colorScale(d.key));
在本節(jié)中,我們要做的最后一件事是向流圖添加軸和標(biāo)簽。我們開始聲明軸生成器 d3.axisLeft() 并將 yScale 作為引用傳遞。然后,我們使用 .call() 方法將軸元素附加到 SVG 組中。
const leftAxis=d3.axisLeft(yScale);
innerChart
.append("g")
.call(leftAxis);
我們可能會(huì)省略 x 軸,因?yàn)榱鲌D與下面的堆疊條形圖水平對(duì)齊,并且此圖表具有相同的 x 軸。但我們將利用這個(gè)機(jī)會(huì)討論如何擴(kuò)展軸上的刻度以在圖表后面創(chuàng)建網(wǎng)格。
首先,我們需要記住,SVG 元素是按照它們?cè)?SVG 容器中出現(xiàn)的順序繪制的。因此,如果我們希望網(wǎng)格出現(xiàn)在流線圖后面,我們需要先繪制它。這就是為什么以下代碼片段應(yīng)位于追加流圖路徑的代碼片段之前的原因。
到目前為止,生成底部軸的代碼與用于堆疊條形圖的代碼相同,包括 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);
要將即時(shí)報(bào)價(jià)轉(zhuǎn)換為網(wǎng)格,我們所要做的就是使用 tickSize() 方法擴(kuò)展它們的長(zhǎng)度。通過(guò)這種方法,我們給即時(shí)報(bào)價(jià)一個(gè)對(duì)應(yīng)于內(nèi)部圖表高度的長(zhǎng)度,乘以 -1 使它們向上增長(zhǎng)。請(qǐng)注意,我們還可以首先避免平移軸,并將此長(zhǎng)度設(shè)置為正值,以使刻度線從上到下的方向增長(zhǎng)。每當(dāng)需要水平網(wǎng)格時(shí),此方法也可以應(yīng)用于左軸或右軸。
const bottomAxis=d3.axisBottom(xScale)
.tickValues(d3.range(1975, 2020, 5))
.tickSizeOuter(0)
.tickSize(innerHeight * -1);
最后,我們可以選擇隱藏軸底部的水平線和年份標(biāo)簽,方法是將它們的不透明度定為零。為此,我們使用之前賦予 x 軸容器的類名( x-axis-streamgraph ),并將其用作 CSS 文件可視化中的選擇器.css .正如您在下面的代碼片段中看到的,通過(guò)“ .x-axis-streamgraph path”訪問(wèn)的水平線的不透明度是用stroke-opacity屬性管理的,而我們需要使用填充不透明度來(lái)隱藏年份標(biāo)簽(“ .x-axis-streamgraph文本”)。我們還可以使用 D3 style() 方法來(lái)處理流圖內(nèi)的不透明度.js .
.x-axis-streamgraph path {
stroke-opacity: 0;
}
.x-axis-streamgraph text {
fill-opacity: 0;
}
最后,我們將在左側(cè)軸上方添加一個(gè)標(biāo)簽,以指示此軸所代表的內(nèi)容。如圖 5.2 所示,或者在托管項(xiàng)目 (https://d3js-in-action-third-edition.github.io/visualizing-40-years-of-music-industry-sales/) 上,流圖的標(biāo)簽分為兩行,第一行帶有文本“總收入(百萬(wàn)美元)”,第二行提到“根據(jù)通貨膨脹進(jìn)行調(diào)整”。
我們將使用 SVG 文本構(gòu)建此標(biāo)簽。關(guān)于 SVG 文本,需要了解的一件事是它的行為不像 HTML 文本。例如,如果我們?cè)?HTML 元素中添加文本,文本將根據(jù)水平可用空間自動(dòng)換行或重排。SVG 文本不會(huì)這樣做,每個(gè)文本元素的位置需要單獨(dú)處理。
要操作 SVG 文本中的潛臺(tái)詞,我們可以使用 tspan 元素。將文本分解為多個(gè) tspan,允許使用其 x、y、dx 和 dy 屬性分別調(diào)整其樣式和位置,前兩個(gè)用于參考 SVG 容器的坐標(biāo)系,后兩個(gè)用于參考前一個(gè)文本元素。
在上述所有定義中,請(qǐng)務(wù)必記住,文本基線由其文本錨點(diǎn)屬性水平控制,垂直由其主基線屬性控制。
為了創(chuàng)建我們的標(biāo)簽,我們可以使用位于 SVG 文本中的三個(gè) tspan 元素,如圖 5.17 所示。如果文本元素的主基線屬性設(shè)置為 hang ,則文本將顯示在 SVG 容器原點(diǎn)的正下方和右側(cè)。使用 dx 和 dy,我們可以根據(jù)圖 5.17 分別將第二個(gè)和第三個(gè)跨度移動(dòng)到它們的正確位置。
圖 5.17 tspan 元素允許分別操作副詞項(xiàng)的樣式和位置。我們使用屬性 dx 和 dy 來(lái)設(shè)置相對(duì)于前一個(gè)文本元素的位置。
在下面的代碼片段中,我們將該策略付諸行動(dòng)。首先,我們將一個(gè)文本元素附加到我們的 SVG 容器中,并將其主基線屬性設(shè)置為值 hang ,這意味著文本及其子文本的基線將位于它們的正上方。
我們將文本選擇保存到常量 leftAxisLabel 中,并重復(fù)使用它將三個(gè) tspan 元素附加到文本容器中。我們將第一個(gè)tspan的文本設(shè)置為“總收入”,第二個(gè)tspan設(shè)置為“(百萬(wàn)美元)”,第三個(gè)tspan設(shè)置為“經(jīng)通貨膨脹調(diào)整”。
默認(rèn)情況下,tspan 元素一個(gè)接一個(gè)地顯示在同一水平線上。保存您的項(xiàng)目并查看標(biāo)簽進(jìn)行確認(rèn)。
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");
要將第二個(gè) tspan 稍微向右移動(dòng),我們可以設(shè)置其 dx 屬性并為其指定值 5。要將第三個(gè) tspan 移動(dòng)到第一個(gè)和第二個(gè) tspan 下方,我們可以使用 y 或 dy 屬性并為其指定值“20”。在這種特殊情況下,這兩個(gè)屬性將具有相同的效果。最后,如果我們希望第三個(gè) tspan 的左側(cè)與 SVG 容器的左邊框?qū)R,最好使用 x 屬性并將其設(shè)置為零。
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 元素用于將不同的樣式應(yīng)用于文本的一部分。例如,我們可以降低第二個(gè)和第三個(gè) tspan 元素的不透明度,使它們呈灰色,并減小第三個(gè) tspan 的字體大小,因?yàn)榕c標(biāo)簽的其余部分相比,它傳達(dá)了次要信息。
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");
我們的流圖的第一次迭代現(xiàn)在已經(jīng)完成,如圖 5.18 所示。當(dāng)此類圖表的垂直基線位于零時(shí),我們通常將其命名為堆積面積圖,而流線圖的面積往往位于中心基線周圍。在下一小節(jié)中,我們將討論如何更改圖表的基線。但在我們到達(dá)那里之前,觀察堆疊條形圖和堆疊面積圖在這一點(diǎn)上的相似之處很有趣。
圖 5.18 我們流線圖的第一次迭代,也可以命名為堆積面積圖。
通過(guò)控制序列的堆疊順序以及它們?cè)诹慊€周圍的垂直定位方式,我們可以將堆積條形圖和堆積面積圖更進(jìn)一步。此級(jí)別的控制是通過(guò) order() 和 offset() 訪問(wèn)器函數(shù)實(shí)現(xiàn)的,這兩個(gè)函數(shù)都應(yīng)用于堆棧布局生成器。
讓我們首先看一下 order() 訪問(wèn)器函數(shù),它控制形狀垂直堆疊的順序。D3 有六個(gè)內(nèi)置訂單,可以作為參數(shù)傳遞,如圖 5.19 所示。
d3.stackOrderNone 是默認(rèn)順序,這意味著如果未設(shè)置 order() 訪問(wèn)器函數(shù),則應(yīng)用該順序。它按與 keys 數(shù)組中列出的順序相同的順序堆疊對(duì)應(yīng)于每個(gè)系列的形狀,從下到上。d3.stackOrderReverse顛倒了這個(gè)順序,從底部的最后一個(gè)鍵開始,到頂部的第一個(gè)鍵結(jié)束。
d3.stackOrderAscending 計(jì)算每個(gè)序列的總和??偤妥钚〉男蛄形挥诘撞?,其他序列按升序堆疊。同樣,d3.stackOrder降序?qū)⒖偤妥畲蟮男蛄蟹旁诘撞?,并按降序堆疊序列。
最后兩個(gè)訂單計(jì)算每個(gè)序列達(dá)到其最大值的指數(shù)。d3.stackOrderAppearance 按序列達(dá)到峰值的順序堆疊序列,這對(duì)于可讀性非常有用,尤其是對(duì)于基線為零的堆棧。另一方面,d3.stackOrderInsideOut 將峰值最早的序列定位在圖表的中間,將最新峰值的序列放在外面。此順序非常適合形狀圍繞中心基線分布的流線圖。
圖 5.19 D3 允許使用 order() 訪問(wèn)器函數(shù)控制形狀堆疊的順序。在這里,我們看到堆積區(qū)域的示例,但相同的原則適用于堆積條形圖。
堆棧布局的另一個(gè)訪問(wèn)器函數(shù)稱為 offset() ,控制圖表零基線的位置以及形狀在其周圍的分布方式。D3 有五個(gè)內(nèi)置偏移量,如圖 5.20 所示。
d3.stackOffsetNone 將所有形狀定位在零基線上方。它是默認(rèn)偏移量。
以下三個(gè)偏移分布基線上方和下方的形狀。d3.stackOffsetDiverging 將正值定位在基線上方,負(fù)值定位在基線下方。此偏移最適合堆積條形圖。d3.stackOffsetSilhouette 將基線移動(dòng)到圖表的中心。d3.stackOffsetWiggle的作用類似,但優(yōu)化了基線的位置,以最小化擺動(dòng)或序列的交替上下移動(dòng)。這三個(gè)偏移需要調(diào)整垂直刻度的域以適應(yīng)基線的位置。
最后,d3.stackOffsetExpand 規(guī)范化 0 到 1 之間的數(shù)據(jù)值,使每個(gè)索引的總和為 100%。歸一化值時(shí),垂直刻度的域也在 0 和 1 之間變化。
在創(chuàng)建堆疊布局時(shí),我們通常會(huì)組合順序和偏移量以達(dá)到所需的結(jié)果。雖然對(duì)于我們何時(shí)應(yīng)該使用順序或偏移量沒有嚴(yán)格的規(guī)定,但目標(biāo)應(yīng)始終是提高可視化的可讀性和/或?qū)⒆⒁饬性谖覀兿胍獜?qiáng)調(diào)的故事上。
對(duì)于本章的項(xiàng)目,我們將使用 order() 和 offset() 訪問(wèn)器函數(shù)將堆積面積圖轉(zhuǎn)換為具有中心基線和堆積條形圖以表示相對(duì)值(介于 0 和 100% 之間)的流圖。
在我們開始之前需要注意的一件事是,order() 和 offset() 訪問(wèn)器函數(shù)可以顯著更改注釋數(shù)據(jù)集中攜帶的值。例如,通過(guò)將堆積面積圖轉(zhuǎn)換為流圖,所表示的銷售價(jià)值將不再在 24 到 000,12 之間變化,而是在 -000,12 和 000,3 之間變化。同樣,如果我們使用 d0.stackOffsetExpand 來(lái)規(guī)范堆疊條形圖顯示的銷售額,則注釋數(shù)據(jù)將包含在 1 到 <> 之間。在設(shè)置垂直刻度的域時(shí),必須考慮這些不同的值。
考慮不同 offset() 訪問(wèn)器函數(shù)帶來(lái)的域變化的一種簡(jiǎn)單方法是確保我們始終計(jì)算注釋數(shù)據(jù)集中的最小值和最大值,并相應(yīng)地設(shè)置域。
在示例 5.11 中,我們首先聲明兩個(gè)空數(shù)組,一個(gè)存儲(chǔ)每個(gè)序列的最小值,另一個(gè)存儲(chǔ)最大值。然后我們遍歷帶注釋的數(shù)據(jù)集,使用 d3.min() 和 d3.max() 找到每個(gè)序列的最小值和最大值,并將它們推送到相應(yīng)的數(shù)組中。最后,我們從每個(gè)數(shù)組中提取最小值和最大值,并使用它們來(lái)設(shè)置域。
此策略可應(yīng)用于流圖和堆積條形圖。對(duì)于堆積條形圖,您可能希望從比例聲明中刪除 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();
完成此修改后,您可以自由測(cè)試偏移值的任何順序,并且 yScale 的域?qū)⒆詣?dòng)調(diào)整。
現(xiàn)在,要將堆疊面積圖轉(zhuǎn)換為流圖,我們所要做的就是將 order() 和 offset() 訪問(wèn)器函數(shù)鏈接到之前聲明的堆棧生成器。在這里,我們使用訂單 d3.stackOrderInsideOut 與偏移量 d3.stackOffsetSilhouette 結(jié)合使用。我們鼓勵(lì)您測(cè)試一些組合,以了解它們?nèi)绾斡绊憯?shù)據(jù)表示。
const stackGenerator=d3.stack()
.keys(formatsInfo.map(f=> f.id))
.order(d3.stackOrderInsideOut)
.offset(d3.stackOffsetSilhouette);
流線圖在美學(xué)上令人愉悅,它們肯定會(huì)吸引注意力。但它們也更難閱讀。當(dāng)您想要概述現(xiàn)象隨時(shí)間推移的演變時(shí),流圖是一個(gè)很好的選擇。但是,如果您希望讀者能夠精確地測(cè)量和比較值,堆疊條形圖或成對(duì)條形圖是更好的選擇。工具提示還可以幫助提高流圖的可讀性。我們將在第 7 章中構(gòu)建一個(gè)。
同樣,我們通過(guò)將其偏移量設(shè)置為 d3.stackOffsetExpand 來(lái)修改堆積條形圖,這將規(guī)范化 0 到 1 之間的銷售值。我們還將順序設(shè)置為 d3.stackOrderDescending,以強(qiáng)調(diào) CD 格式在 2000 年左右如何主導(dǎo)市場(chǎng)。再次嘗試一些組合,看看它如何改變圖表傳達(dá)的故事焦點(diǎn)。
const stackGenerator=d3.stack()
.keys(formatsInfo.map(f=> f.id))
.order(d3.stackOrderDescending)
.offset(d3.stackOffsetExpand);
在最后一節(jié)中,我們將討論如何使用傳統(tǒng)的 HTML 元素輕松構(gòu)建圖例,并將通過(guò)在堆疊條形圖下方放置顏色圖例來(lái)將其付諸實(shí)踐。圖例是數(shù)據(jù)可視化的重要組成部分,可幫助讀者解釋他們所看到的內(nèi)容。
通常,圖例涉及文本,我們知道 SVG 文本并不總是便于操作。如果您查看我們將在圖 5.21 中構(gòu)建的顏色圖例,您會(huì)發(fā)現(xiàn)它由一系列彩色方塊和標(biāo)簽組成,與堆疊條形圖水平居中。使用 SVG 元素構(gòu)建此圖例將涉及計(jì)算每個(gè)矩形和文本元素的確切位置。這是可能的,但有一種更簡(jiǎn)單的方法。
圖 5.21 我們將在本節(jié)中構(gòu)建的顏色圖例,位于堆積條形圖下方。
D3 不僅用于控制 SVG 元素。它可以創(chuàng)建和操作任何 DOM 元素。這意味著我們可以使用傳統(tǒng)的HTML元素構(gòu)建圖例,并使用CSS來(lái)定位它們。有很多方法可以繼續(xù),但這樣的圖例要求結(jié)構(gòu)化為 HTML 無(wú)序列表 ( <ul></ul> )。帶有標(biāo)簽的每個(gè)顏色組合都可以存儲(chǔ)在 <li></li> 元素中,其中一個(gè) <span></span> 元素保存顏色,另一個(gè)元素包含標(biāo)簽,如以下示例所示。
<ul>
<li>
<span> color 1 </span>
<span> label 1 </span>
</li>
<li>
<span> color 2 </span>
<span> label 2 </span>
</li>
...
</ul>
要使用 D3 構(gòu)建此 HTML 結(jié)構(gòu),我們轉(zhuǎn)到文件圖例.js并開始在函數(shù) addLegend() 中工作。在下面的代碼片段中,我們選擇帶有一類 legend-container 的 div,該類已存在于索引中.html .我們將一個(gè) ul 元素附加到這個(gè) div 中,并給它一類顏色圖例。
然后,我們使用數(shù)據(jù)綁定模式為 formatInfo 數(shù)組中包含的每種格式附加一個(gè) li 元素,該數(shù)組在共享常量中可用.js .我們將此選擇保存到一個(gè)常量中 命名 圖例項(xiàng) .
我們調(diào)用 legendItems 選擇并將 span 元素附加到其中,并根據(jù)相關(guān)的音樂格式設(shè)置跨度的背景顏色屬性。為此,我們可以直接從 格式信息 或調(diào)用色標(biāo)。最后,我們附加另一個(gè) span 元素并將其文本設(shè)置為當(dāng)前格式的標(biāo)簽鍵。
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
如果您應(yīng)用的類名與上一代碼段中使用的類名相同,則圖例應(yīng)自動(dòng)如圖 5.21 所示。這是因?yàn)橐韵聵邮揭言?base 中設(shè)置.css .請(qǐng)注意我們?nèi)绾问褂?CSS flexbox 屬性 (https://css-tricks.com/snippets/css/a-guide-to-flexbox/) 來(lái)處理圖例的布局。我們不會(huì)花時(shí)間解釋這個(gè)樣式片段,因?yàn)槟赡苁煜SS,這不是本書的重點(diǎn)。這里的主要要點(diǎn)是,有時(shí)傳統(tǒng)的HTML元素和CSS樣式比SVG更容易操作,我們可以使用D3來(lái)綁定數(shù)據(jù)和操作任何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;
}
您現(xiàn)在知道如何使用 D3 布局,如餅圖和堆棧布局。在第7章中,我們將把這個(gè)項(xiàng)目變成一個(gè)交互式可視化。如果您接下來(lái)想這樣做,請(qǐng)隨時(shí)直接去那里。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。