去年參加一堂D3.js的課程,在初步學完網頁前後端之後,發現在一堆資料中如何視覺化是一大學問,所以就開始D3.js的學習路程,簡單的紀錄一下筆記,給自己未來專案開發上的參考。
何謂D3.js?
D ata-D riven D ocuments(資料驅動的文件)
缺點是難度高,優點是彈性高,可以更自由的客製化圖表
白話來說,就是用js函式 把資料 與文件 中的視覺元素綁定 在一起
HTML/CSS/Javascript 文件其實就是網站,也可以說是HTML:
HTML (H yperT ext M arkup L anguage),超文本標籤語言
ML : 標籤語言,ex: <a>
、<br>
…
HT : 超文本,不同的html透過標籤互相連結,ex: a.html 連到b.html
CSS基本選擇器有四類:
標籤選擇器
類別選擇器
ex: td.circle{background:yellow}
選出具有circle類別的td
ID選擇器
ex: #circle{background:yellow}
屬性選擇器
ex: [rowspan="2"][colspan="2"]{color:red}
選出具有rowspan=”2”且 colspan=”2”的標籤
CSS進階選擇器:
孩子選擇器
ex: div>p{屬性1:值1,...}
選出div的下一層所有p
子孫選擇器:
ex: div p{屬性1:值1,...}
選出div底下所有p
CSS優先權:
從左至右代表優先到籠統
Style/ID/Class,虛擬類別,屬性/標籤
若是同階級,則是後者為大
,後者決定CSS樣式
最專一是使用 !important
javascript的random:
產生1至M 之間的數: 1 ≤ Math.floor(Math.random()*M+1) ≤ M
產生N至M 之間的數: N ≤ Math.floor(Math.random()*(M-N+1)+N) ≤ M
D3.js中常用的匿名函式:
1 var showMsg = function(msg1, msg2){...}
載入D3.js 引入d3.js,官網 ,V3的版本可以支援較好的寫法,建議用v3。
1 <script src="https://d3js.org/d3.v3.min.js"></script>
鏈結語法
1 d3.select('body').append('div').text("Hi");
1 2 3 <body> <div>Hi</div> </body>
增加class
1 2 3 d3.select("p") .append("div") .classed('ball',true) //給定一個叫ball的類別
增加屬性
1 2 3 d3.select("body") .append("div") .attr('class','ball') //新增一個class屬性,名稱為ball
SVG繪圖 1 2 3 <svg width="400" height="300"> 圖案 </svg>
圖案又分為rect
、circle
、ellipse
…等等,注意的是外圍一定要用svg包住 才能顯示圖案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 長方形 <rect x="100" y="100" width="150" height=“50"></rect> // 圓形 <circle cx="100" cy="100" r="50"></circle> // 橢圓形 <ellipse cx="100" cy="100" rx="150" ry="50"></ellipse> // 線,stroke="顏色"是必填 <line x1="50" y1="30" x2="150" y2="200" stroke="black" stroke-width="2"></line> // 折線,三個點: (0,0)->(200,100)->(400,300) <polyline points="0,0 200,100 400,300" stroke="black" fill="rgba(0,0,0,0)" strokewidth="2"></polyline> // 文字,y的位置是baseline文字基線 <text x="100" y="200" fontsize="40" font-family="arial">要放的文字</text> // 其他屬性: 填色(fill)與外框(stroke)
第四象限 width: 向右為正 height: 向下為正
繪圖工具轉成code
1. 繪製長條圖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 //HTML <script src="https://d3js.org/d3.v3.js"></script> <svg width="400" height="300"></svg> //JS for(var i=0; i<20; i++){ var thisNum = random(20,300); d3.select("svg") .append("rect") .attr({ x: 10, y: 10+12*i, width: thisNum, height: 10, fill: "red" }); d3.select("svg") .append("text") .attr({ x: thisNum+15, y: 20+12*i, "font-size": 12 }).text(thisNum); } function random(n,m){ return Math.ceil(Math.random()*(m-n)+n); }
2. 繪製折線圖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 //HTML <script src="https://d3js.org/d3.v3.js"></script> <svg width="400" height="300"></svg> //JS var lastNum = 0; var points = "10,10 "; for(var i=0; i<20; i++){ var thisNum = random(20,300); points = points+(10+thisNum)+","+(10+12*(i+1))+" "; d3.select("svg") .append('circle') .attr({ cx: 10+thisNum, cy: 10+12*(i+1), r: 3, fill: "red" }); d3.select("svg") .append("text") .attr({ x: thisNum+15, y: 25+12*i, "font-size": 12 }).text(thisNum); lastNum = thisNum ; } points = points+"10,"+(21*12+10); d3.select("svg") .append("polyline") .attr({ "points": points, "fill": "rgba(0,0,0,0)", "stroke": "#333", }); function random(n,m){ return Math.ceil(Math.random()*(m-n)+n); }
D3讀取資料 csv 1 2 3 4 d3.csv("檔名.csv", function(dataSet){ console.table(dataSet); //用成table的方式印出 //dataSet[i].欄位名稱1... });
若要處理csv的資料,要在d3.csv(){..}裏頭處理,不能丟到外面再處理,否則會有時間差,導致執行順序不同而有差錯。
json 1 2 3 4 d3.json("檔名.json", function(dataSet){ console.table(dataSet); //用成table的方式印出 //dataSet[i].欄位名稱1... });
D3綁定資料 用.datum()
綁定**單筆資料(變數)**,可以存在data
變數裏頭
1 2 3 4 5 6 7 8 9 10 11 var arr = [85, 60, 99, 49, 77, 82]; for(var i=0; i<arr.length; i++){ d3.select("body") .append("div") .datum(arr[i]) //綁定 .text(function(d){ //取出綁定的資料 return d; }); } console.log(selectAll("div"));
用.data()
綁定多筆資料(陣列)**, 不再需要for迴圈,用selectAll()就好**
1 2 3 4 5 6 7 8 var arr = [85, 60, 99, 49, 77, 82]; d3.select("body") .selectAll("div") .data(arr) .text(function(d){ return d; });
綁定有三種情況:
資料數量 = 視覺元素數量
資料數量 < 視覺元素數量,靠enter
(進入可視化)增加足量元素
資料數量 > 視覺元素數量,靠exit
(離開可視化)移除多餘元素
舉例來說
1 2 3 4 5 6 7 8 9 10 11 12 // 第一種,數量剛好 var selection = d3.select("svg") .selectAll("rect") .data(dataSet); // 第二種,資料太少 selection.enter().append("rect").text(function(d){ return d; }); // 第三種,資料太多 selection.exit().remove();
將以上程式碼分類,用bind()
、render()
兩個函式各司其職:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var arr = [85, 60, 99, 49, 77, 82]; // bind函式一次考慮好三種情況 function bind(data){ var selection = d3.select("body") .selectAll("div") .data(data); selection.enter().append("div"); selection.exit().remove(); } // render負責把視覺元素畫出來 function render(){ d3.selectAll("div").text(function(d, i){ //順序不可改,先data再index return i+":"+d; }) } bind(arr); //執行綁定 render();
綁定資料的長條圖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 <body> <input center type="button" value="新增" onclick="update()"> <input type="button" value="移除" onclick="remove()"> <script src="https://d3js.org/d3.v3.min.js"></script> <script> var arr = [30, 80, 15, 60, 67, 99]; var w = 600; var h = 400; var p = 100; //呼叫函式 svg(); bind(arr); render(); //繪圖 function svg() { d3.select("body") .append("svg") .attr({ "width": w, "height": h }); } //綁定資料,每個圖案都要考慮三種狀況 function bind(data) { //長方形 var selection = d3.select("svg") .selectAll("rect") .data(data); selection.enter().append("rect"); selection.exit().remove(); //文字 var selection_text = d3.select("svg") .selectAll("text") .data(data); selection_text.enter().append("text"); selection_text.exit().remove(); } //渲染到視覺元素上 function render() { //長方形 d3.selectAll("rect") .attr({ "x": function (d, i) { return p + 43 * i; }, "y": function (d) { return h - p - d; }, "width": 40, "height": function (d) { return d; }, "fill":function(d){ if(d>70){ return "red" } else{ return "lightgreen" } } }); //文字 d3.selectAll("text") .attr({ "x": function (d, i) { return p + 43 * i+7; }, "y": function (d) { return h - p+25; }, }).text(function(d){ return d; }); } //新增按鈕觸發 function update(){ var num = random(10,100); arr.push(num); bind(arr); render(); } //移除按鈕觸發 function remove(){ arr.pop(); bind(arr); render(); } function random(N,M){ return Math.ceil(Math.random()*(M-N)+N) } </script> </body>
比例尺(Scale) 縮小or放大 domain
是輸入資料的範圍,range
是輸出資料的範圍,rangeRound
則是把輸出變成四捨五入
1 2 3 4 5 var xScale = d3.scale.linear() .domain([0, 1000]) //輸入 .range([0, 100]); //輸出 console.log(xScale(455)); //45.5
Clamp設定上下限 多增加一個clamp(左右夾緊)**, 當輸入的值超過輸入範圍,會自動把輸出限於輸出上下限**
1 2 3 4 5 6 7 8 //超過範圍自動變成上下限 var xScale = d3.scale.linear() .domain([0, 1000]) //輸入 .range([0, 100]) //輸出 .clamp(true); //上下限 console.log(xScale(-100)); //0 console.log(xScale(1200)); //1000
用d3.scale.linear()
修改random(N,M)亂數函式
1 2 3 4 5 6 7 8 function random(N, M){ var rScale = d3.scale.linear() .domain([0,1]) .rangeRound([N,M]); return rScale(Math.random()); } console.log(random(20,100))
d3.min()/d3.max() 當不知道原始資料的範圍的時候,可以使用d3.min()
、d3.max()
來找出最小值跟最大值
1 2 3 4 var arr = [40, 50, 88]; var xScale = d3.scale.linear() .domain([d3.min(arr), d3.max(arr)]) .range([0, 255]);
還可以透過d3.min()
、d3.max()
函式去比較物件內部其他的屬性
1 2 3 4 5 6 7 8 9 10 var dataSet = [ {name: "Eric", tall: 180, age: 25}, {name: "Ice", tall: 150, age: 15}, ]; d3.max(dataSet, function(d){ return d.tall; }) // 180
所以前面的xScale
可以寫成這樣,讓d3自己去找資料的最小值跟最大值
1 2 3 4 5 6 var dataSet = {略...}; var xScale = d3.scale.linear() .domain([d3.min(dataSet, function(d){return d.屬性;}), d3.max(dataSet, function(d){return d.屬性;})]) .range([輸出最小值, 輸出最大值])
序數比例尺 用d3.scale.ordinal()
來做到序數的功能
1 2 3 4 5 6 7 8 9 10 var index = [0,2,4,6,8]; var color = ["red", "blue", "green", "yellow", "black"]; var xScale = d3.scale.ordinal() .domain(index) .range(color); console.log(xScale(0)); // "red" console.log(xScale(4)); // "green" console.log(xScale(15)); // "red" -> 當index不在範圍內,會回傳第一個
d3內建填色序數 ,用d3.scale.category20()
來產生前20個顏色,也有d3.scale.category10()
來產生前10個顏色,採用認領的機制,先出現的元素先認領顏色
1 2 3 4 5 var fScale = d3.scale.category20(); console.log(fScale(1)); //"#1f77b4" console.log(fScale(2)); //"#aec7e8" console.log(fScale("3")); //"#ff7f0e"
日期比例尺 用d3.time.scale()
來製作日期的比例尺,日期並不是javascript的資料型態,只是一種物件資料型態,所以如果要產生日期,得用new Date(日期)
。
1 2 3 4 5 6 7 8 var xScale = d3.time.scale() .domain([ new Date("2019-01-01"), new Date("2020-08-01") ]) .rangeRound([0, 100]); console.log(xScale(new Date("2020-01-05"))) //64
Axes-座標軸
<g>
是group的縮寫,是用來將元素做分組
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <g> <!-- 第⼀個刻度 --> <g> <line></line> <!-- 第⼀個刻度的直線 --> <text></text> <!-- 第⼀個刻度的⽂文字 --> </g> <!-- 第⼆個刻度 --> <g> <line></line> <!-- 第⼆個刻度的直線 --> <text></text> <!-- 第⼆個刻度的⽂文字 --> </g> ... <!-- 坐標軸的軸線 --> <path></path> </g>
D3內建有座標軸語法,d3.svg.axis()
,共分為五個步驟
1 2 3 4 5 6 7 8 9 10 11 12 13 //第一步-產生軸線 var xAxis = d3.svg.axis() .scale(xScale) .orient("right") //預設是bottom(刻度在底部),還有top,left,right可以選 .ticks(5); //第四步-刻度數量(參考值),D3會自動選擇,不一定會照給定的值 //第二步-畫在svg上 d3.select("svg") .append("g") .classed("axis",true) //第三步-調整CSS樣式(座標軸本⾝是由path,line,text構成) .attr("transform","translate(0,"+(h-padding+25)+")") //第五步-軸線位移 .call(xAxis)
CSS
1 2 3 4 5 6 7 8 9 .axis path, .axis line { fill: none; stroke: black; /*線條顏色*/ shape-rendering: auto; /*讓線很細,比較像座標軸*/ } .axis text { font-size: 11px; fill: blue; }
當座標軸的刻度太擠,可以用.tickFormat()
來調整
1 2 3 4 5 6 7 var xAxis = d3.svg.axis() .scale(xScale) .orient("right") .ticks(5) .tickFormat(function(d){ //把座標值修正 return d/1000000+'G'; })
互動式資料視覺化 取出不重複的元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var arr1=["A","B","A","C"]; var uniArr = unique(arr1); console.log(uniArr); //["A", "B", "C"] function unique(array){ var n = []; for(var i = 0; i < array.length; i++){ if (n.indexOf(array[i]) == -1){ n.push(array[i]); } } return n; }
當陣列中是包含物件,則可以用map
函式來對陣列中的各元素進行操作,並返回一個一樣大小的新陣列
1 2 3 4 5 6 7 8 9 10 11 12 13 var arr1 = [ {city: "台北市", cid: "A"}, {city: "台中市", cid: "B"}, {city: "台北市", cid: "A"}, {city: "基隆市", cid: "C"} ]; var arr2 = arr1.map(function(d){ return d.city; }); var arr3 = unique(arr2); console.log(arr3) //["台北市", "台中市", "基隆市"]
下拉式選單 1 2 3 4 5 6 7 8 9 10 11 12 <select> <option value="Taipei">台北</option> <option value="Taoyuan">桃園</option> <option value="Hsinchu">新⽵</option> <option value="Miaoli">苗栗</option> ... </select> d3.select("select").on("change",function(){ var value = d3.select("select").property("value"); //property:像是表單等等動態的元素屬性,與attr類似 console.log(value) })
提示框 第一種方法,一般網頁顯示的提示框
1 2 3 4 // html原始就有提示框的功能->title d3.selectAll("circle").append("title").text(function(d){ return d.city+"\r\n"+d.industry+"\r\n"+d.amount; });
第二種方法,自製提示框
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // HTML <div id="tooltip" class="hidden"> <p><strong id="city">Hello</strong></p> <p id="industry">tooltip</p> </div> // CSS #tooltip{ position: absolute; /* left: 20px; */ /* top: 100px; */ background: #fff; width: 150px; height: auto; padding: 0px 10px; border-radius: 5px; box-shadow: 5px 5px 10px rgba(0,0,0,0.3); } #tooltip.hidden{ lay: none; } // Javascript d3.selectAll("circle") .attr({ cx: ... }) .on("mouseover", function(d){ var tooltip = d3.select("#tooltip") .style({ left: x該放的位置+"px", top: y該放的位置+"px" }) //替換tool3p內容(選擇其id後,修改內容) d3.select("#tooltip").classed("hidden",false); }) .on("mouseout",function(d){ d3.select(“#tooltip").classed("hidden",true); });
layout佈局 圓餅圖
d3.layout.pie()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 //佈局 var pie = d3.layout.pie().value(function(d) { return d.crude_rate; //資料占比依據 }); //綁定 var selection = d3.select("svg") .selectAll("g.arc") .data(pie(dataSet)); var g_arc = selection.enter().append("g").attr("class","arc"); g_arc.append("path"); g_arc.append("text"); selection.exit().remove(); //繪圓餅圖 var outerR = 300; var innerR = 100; var w = 900; var h = 600; var arc = d3.svg.arc() .outerRadius(outerR) .innerRadius(innerR); var fScale = d3.scale.category20(); d3.selectAll("g.arc") .attr("transform", "translate("+w/2+","+h/2+")") //圓餅圖圓心在網頁上的位置 .select("path") .attr("d", arc) .style("fill", function(d,i) { return fScale(i); }); //把綁定到g.arc裏頭的資料用arc的外觀屬性丟給path來畫 d3.selectAll("g.arc") .select("text") .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; }) //arc.centroid 計算並回傳此元素中心位置(重心) .attr({ "text-anchor":"middle", dy: 20, //y的移動距離 dx: 20 //x的移動距離 }) .text(function(d){ return d.data.category+ " "+d.data.crude_rate; })
排序
d3.ascending(a, b) : 升序d3.descending(a, b) : 降序
1 2 3 4 var ARRAY = [{num: 4},{num: 6},{num: 8}]; var newARR = ARRAY.sort(function(a, b){ return d3.descending(a.num, b.num); });
泡泡圖 跟圓餅圖不一樣,要先產生結構再丟給他數據
d3.layout.pack()
1 2 3 4 5 6 var pack = d3.layout.pack() .size([寬, ⾼]) //svg畫布的長寬 .padding( 泡泡間距 ) .value( 節點數值 ) .children( ⼦節點 ) .nodes(root) //丟資料
資料(root)需要先結構化(巢狀化),每一層都要有children
跟value
1 2 3 4 5 6 7 8 9 10 var root = { value: (數值大小), children: [ { value: … , children:[] }, {…}, …… ] }
如何結構化? D3.js可以透過以下程式碼來將資料巢狀化
1 2 3 4 d3.nest() .key() //分類節點,要傳入匿名函式 .key() //分類節點,要傳入匿名函式 .entries() //來源資料
舉例來說,一個arr
裝有六筆資料,每筆資料有三個欄位:value
、char
、num
,今天用D3.nest將他巢狀化,裏頭的key用匿名函式決定以哪一個欄位做分類節點 ,第一層的key用char
欄位,第二層的key用num
欄位,巢狀化之後會回傳一個物件(Object)。
觀察這個物件的結構,可以看到會有兩層結構,每層結構都是一組鍵值配對(Key-Value pair)。
因為第一層用char
分類完會只有三種A
、B
、C
,所以第一個螢光筆處只有三筆,接著每一筆裏頭藍色的數量就是資料的數量,因為A只有一筆所以藍色只有一筆,B有兩筆所以藍色有兩筆,以此類推…
而第二層再用num
分類,每一筆藍色的資料裏頭就是該組依照num
分類的數量,因為A只有一筆,所以螢光筆處只有一筆,B有兩筆,所以螢光筆處有兩筆,以此類推…
最後,最裏頭的value
就是該筆資料存放的數據。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var arr = [ {value: 1, char: "A", num: 1}, {value: 2, char: "B", num: 1}, {value: 3, char: "B", num: 2}, {value: 4, char: "C", num: 1}, {value: 5, char: "C", num: 2}, {value: 6, char: "C", num: 3}, ]; var nested_a = d3.nest() .key(function(d){ return d.char; }) .key(function(d){ return d.num; }) .entries(arr); console.log(nested_a);
除此之外,資料巢狀化還有以下三種常見的作法
.rollup(function): 彙總 .sortKeys(function): key排序 .sortValues(function): Values排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //彙總 d3.nest() .key() .rollup( function(v){ return v.length; //也可以自己創造欄位,ex:{a:v.length, b:12} }) .entries() //key排序 d3.nest() .key() .sortKeys(d3.descending) .entries() //Values排序 d3.nest() .key() .sortValues(d3.descending) .entries()
上例來說,以char
分類後的values會是A,B,C三個的數量,[1,2,3]。彙總的物件結構:
以d3.descending
將Key降序的物件結構
地圖 分為以下四個步驟
取得地理資料檔 SHP
資料格式轉換SHP->JSON
D3讀入JSON再綁定<path>
開始操作: 填色、位置標示、互動
STEP1: 取得地理資料檔 SHP
世界地圖下載: https://www.naturalearthdata.com/ 台灣縣市界線地圖下載: https://data.gov.tw/dataset/7442
進入世界地圖網站 > 點選Downloads分頁 > 選擇Cultural類別
STEP2: 資料格式轉換SHP->JSON
轉檔的網站: Mapshaper
(1) 上傳 .shp & .dbf (2) Simplify 調整壓縮 (3) Export 輸出 GeoJSON 或 TopoJSON
STEP3: D3讀入JSON再綁定<path>
第一種: GeoJSON
市面上流通較常見的格式
自己創造GeoJSON的網站: http://geojson.io/#map=2/20.0/0.0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //1.地理資料檔: GeoJSON d3.json("tw-map-geo.json”, function(mapDataSet) { bind(mapDataSet); render(); }); function bind(geoRoot){ // 2.地理投影器: 設定投影方式:麥卡托、定位點([經度,緯度])、縮放(scale) var projection = d3.geo.mercator().center([121,24]).scale(8000); // 3.路徑產生器: d3.geo.path() var path = d3.geo.path().projection(projection); // 綁定path與載入的地理資料(features:每一地理區劃) var selection = d3.select("svg").selectAll("path").data(geoRoot.features); selection.enter().append("path").classed("map-boundary", true) .attr("d", path); selection.exit().remove(); }
第二種: TopoJSON
D3.js的作者自己開發的格式,因為多了共享邊(arcs),每個邊界只會畫一次,所以檔案較小,較適合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // -----------------------HTML--------------------- <script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.2/topojson.min.js"></script> // -----------------------javascript--------------------- //1.地理資料檔: TopoJSON d3.json("tw-map-topo.json", function(mapDataSet) { bind(mapDataSet); render(); }); function bind(topoRoot){ // 2.地理投影器: 設定投影方式:麥卡托、定位點([經度,緯度])、縮放(scale) var projection = d3.geo.mercator().center([121,24]).scale(8000); // 3.路徑產生器: d3.geo.path() var path = d3.geo.path().projection(projection); // 4.Topo轉Geo: 使⽤topojson.js做檔案格式轉換 var geoRoot = topojson.feature(topoRoot, topoRoot.objects["COUNTY_MOI_1090820"]); //先console.log觀察 // 綁定path與載入的地理資料(features:每一地理區劃) var selection = d3.select("svg").selectAll("path").data(geoRoot.features); selection.enter().append("path").classed("map-boundary", true) .attr("d", path); selection.exit().remove(); }