over 1 year ago

最近想要在網頁上操作SVG,原本想要使用GSAP.js,可是發現不是Open Source,所以就打算跳來使用D3玩玩看。
目前D3版本是4.x,而網路上教學大多是3.x,其中API改了蠻多的,這篇主要是紀錄語法的修正與實作上遇到的困難

教學資源

SVG D3.js - 起手式:這是我看過寫最好的D3入門教材,介紹相當仔細且循序漸進,非常值得看的教學。
D3.js数据可视化系列教程】--(一)简介:這是對岸的文章,我覺得在入門方面沒有上一篇仔細,但是他後續有提供大量實作圖表的程式碼解析,包含圓餅圖、表格樹、冰狀圖、桑基圖等,註解寫得很完整,用來進階學習相當不錯。
D3 官網:老實說我覺得官網的document沒有很完整,教學文件也沒有很好 :/ 範例很多,但出處有點亂XD 只有作者的作品集整理得比較好

學習流程建議先把《SVG D3.js - 起手式》系列文看完,有個基本認知後,在開始看其他人撰寫的範例程式。
以下是我個人學習筆記,而非正式的教學文

select 與append 創造物件

D3可以透過select和selectAll選取DOM物件,有點像是jQuery語法也符合chain function,可以methods串連;
最基本的在HTML的<body>中宣告一個svg物件,在js中宣告let svg = d3.select("svg")
選擇svg後最基本可以設定attr或是style,前者是SVG圖片格式專有的屬性與定義方式,後者則是CSS;D3 V4可以支援DOM/SVG/Canvas三種繪製方式,但我們還是主要專注於DOM/SVG。
最基本我們可以設定長寬和外框。

svg.attr(
  'width', width,
).attr(
  'height', height,
).style(
  'border','2px solid black'
);

接著我們在svg中創造SVG元素,如rect/circle/line/path/text等,這部分可以直接使用append

//創建一顆小綠圓

svg.append("circle").attr("cx", 400).attr("cy", 400).attr("r",50).attr("fill", "green");

至於SVG的基本元素與屬性有哪些,可以參考oxxo studio裡的SVG教學,這是我看到現在包含中英文最完整的SVG教學(D3入門也很精彩,可惜少了一些應用)

先前提到D3也可以select DOM Element,方法大致上差不多

//在div新增一個class為someclass的\<p>且顏色為紅色

select("div").append("p").attr('class', 'someclass').style("color","red")....

另外有一個很詭異的是 attr()與style()一次只能設定一個屬性,不能傳入物件實字 有點麻煩啊OTZ

data

D3全名為Data-Driven Documentation,換言之結合Data無疑是D3的核心,這裡先介紹最基本的陣列。
呼叫data(dataSource)時D3會將資料打包,組成一個新的物件,此時data集合和select的元素集合是不相干的;
接著有三個函式可以呼叫 updateenterexit,這三個函示代表如何將data集合與select的元素集合串連起來
最常用的enter表示 如果select元素不存在則創建新物件,update表示select元素存在就更新data,exit則剛好是刪除多餘元素。
經過enter後,data已經被塞到各個元素裡頭了,如果要讀取資料可以傳入function(d){},其中的d就是資料囉。

//創造一組包含x,y物件的陣列,長度為十 [{x,y}]

let data = new Array(10).fill(0);
data = data.map((v, i)=>{
  return({x:i, y:Math.floor(Math.random(0,100)*100)});
})

//在svg底下選擇所有class為bar的物件,接著傳入data,enter表示

//如果bar不存在也無仿,rect會創建在svg底下

//enter()後資料放入各自節點中,而append()後節點產生,之後使用attr()/style()設定節點的表徵

svg.selectAll('.bar').data(data).enter().append('rect')
  .attr('width', (d)=>scaleX(d.y))
  .attr("height",30)
  .attr("x", 20)
  .attr("y", (d)=>scaleY(d.x))
  //如果d.y小於60就顯示為紅色

  .attr("fill", (d)=>{if(d.y<60)return 'red'})
  .attr("transform","translate(0,-30)");
  
//打印出來看最實在

console.log(svg.selectAll('.bar').data(data))
console.log(svg.selectAll('.bar').data(data).enter())
console.log(svg.selectAll('.bar').data(data).enter().append("text"))

D3的常用起手式svg.selectAll('').data(data).enter().append()....
更進階的select與data就請看另一篇文章D3 - 深入了解Selection與Data綁定

Scale

通常資料與畫面元素長度不會是1:1,例如說考卷分數是0~100,但是我想要對應0~600px表示分數,此時就可以用scale
這裡我是定義X軸與Y軸對應資料的放大比率

//scaleLinear()為D3 v4函式,表示線性放大,也是最常用的scale函式

//後面帶兩個重要子函式 domain和 range,對應就是 值域=>對應域

//d3.max()可以找出陣列中最大值

let scaleX = d3.scaleLinear().range([0, height-50]).domain([0, d3.max(data, (d)=>d.y)])
let scaleY = d3.scaleLinear().range([width-40, 0]).domain([0, d3.max(data, (d)=>d.x)+1])

在range中不是直接放height或width,還要加上padding才不會讓scale剛好塞滿svg圖表,這樣很怪。

Axis 加入座標軸

加入座標軸可以用axis + {Top/Bottom/Left/Right},上下表示 座標標籤在座標軸上方或下方,此時座標軸自動打橫;左右打直。

//X軸自然是要打橫的,axisBottom放入先前定義的scaleX,ticks(num)表示座標軸切成幾等分

//此時axisX是一個由d3回傳的複雜函式,d3會自動幫忙生成軸線與標籤的函式

let axisX = d3.axisBottom(scaleX).ticks(10);
//在svg下產生一個group g並呼叫axisX,這裡的call和JS中的call() method是一樣的,綁定this帶入函式中

svg.append('g').call(axisX)
  .attr("transform","translate(20," + (height-30) +")"); 

let axisY = d3.axisLeft(scaleY).ticks(10);
svg.append('g').call(axisY)
  .attr("transform","translate(20,10)"); 
加入Transition、Tween與Interpolate

加一點特效,增加圖表的互動性,transition用起來跟CSS的transition很像,不過d3可以套用在DOM Element和SVG上,不多贅述;

//加入長條,一樣transition + duration + delay
svg.selectAll('.bar').data(data).enter().append('rect')
  .attr('width', 0)
  .attr("height",30)
  .attr("x", 20)
  .attr("y", (d)=>scaleY(d.x))
  .attr("fill", (d)=>{if(d.y<60)return 'red'})
  .attr("transform","translate(0,-30)")
  .transition()
  .duration(1000)
  .delay(500)
  .attr('width',(d)=>scaleX(d.y));

Interpolate線性內差,也是我覺得最有趣的部分,D3有一套運算線性內差的方式,不單單支援數字,包括日期、顏色、甚至是字串都可以使用,相當的強大!先看一下以下範例(參考 官網d3-interpolate)

//最基本的數字
var i = d3.interpolateNumber(10, 20);
i(0.0); // 10
i(0.2); // 12
i(0.5); // 15
i(1.0); // 20

//日期
var d1 = new Date('2012-01-01');
var d2 = new Date('2012-02-01');
var i = d3.interpolateDate(d1,d2);
console.log(i(0.2), d1, d2);

//最神奇的字串內插
var s1 = "Apple1"
var s2 = "Apple5";
var i = d3.interpolateString(s1,s2);
console.log(i(0.2)); //Apple1.8

var s1 = "Ap1a1ple";
var s2 = "Ap5b5ple";
var i = d3.interpolateString(s1,s2);
console.log(i(0.2));//Ap1.8b1.8ple

tween表示補間動畫,可以在此調整值與參數,通常會結合線性內差做使用,這部分參考SVG D3.js - 直條圖 ( Bar Chart )

/加入長條的文字與轉場特效
svg.selectAll('.foo').data(data).enter().append('text')
  .attr("class", "scoreText")
  .attr("x", 30)
  .attr("y",(d)=>scaleY(d.x)-5)
  .attr("fill","blue")
  .attr("font-family", "sans-serif")
  .attr("font-size", "20px")
  .text(0)
  .transition()
  .duration(1000)
  .delay(500)
  .tween("scoreText", function(d){
    let i = d3.interpolateRound(0, d.y);
    return (t)=>{
      this.setAttribute("x",scaleX(i(t))+30);
      this.textContent = i(t);
    }
  })
總結

製作D3最基本的就是長條圖,從
select() 選定元素 / append() 生成元素 / attr()與style() 設定元素表徵 /
data() enter()將資料傳入元素中並使用 (d)=>{}獲取資料 /
transition()與tween()結合interpolate()線性內差做特效。

接著還有更多類型的圖表與互動方式要繼續學習。

← ReactJS: Under The Hood 影片筆記 - 關於render()到DOM D3 - 深入了解Selection與Data綁定 →
 
comments powered by Disqus