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