JS Day 37: Var, Let and Const, part 2 - Hoisting

Hoisting 提升

Var Hoisting 變數提升

console.log(a); // undefined
var a = 'A';

// ---- 分隔線 ----

// 實際上這段程式碼對編譯器來說是長這樣
var a; // 只有宣告 a 這個變數
console.log(a); // 只有被宣告,但沒有賦予值,自然是 undefined
a = 'A'; // assignment 不會被提升

把上面的例子拆分,這樣子比較好理解。

var a; // 有宣告,但沒有賦予任何值
console.log(a);  // a 會是 undefined

使用 var 跟 let 的結果:

console.log(a); // Undefined
var a = 'A';

console.log(b);  // ReferenceError: b is not defined
let b = 'B'; 

console.log(c);  // ReferenceError: c is not defined
const c = 'C'; 

https://drive.google.com/uc?export=view&id=1ut6WFtMB7-4MUNbgfCZyn24ZzzjFdBxG

undefined =/= not defined
undefined 在記憶體已經有準備空間給它
not defined 沒有被定義

var 的特性,只要繼替體有準備位置,使用 var 會出現 hoisting (undefined),反之使用 let& const 則不會出現 hoisting(not defined),因為變數 b 之前不存在,所以會出錯。

提升 & 函式 (Hoisting & Functions)

函式運算式 (Function Declaration)

延續上方,console.log(a) 會是 undefined

alpha(); // 'Alpha calling...', function 會被提升
console.log(a); // undefined, var a 會被提升,但 assignment 不會
var a = 'A';

function alpha() {
    console.log('Alpha calling...');
};

// output
// Alpha calling...
// undefined
console.log(a); // function a()
var a;
function a() {
    console.log('say a word');
};

為何不是 undefined? 因為 function 被提升了。

類似範例:

aTest(); // 我被提升了
function aTest() {
    console.log('我被提升了');
};

範例1:

var a = 0;

function a() {
    return 1;
};

console.log(typeof a); // number

可以這樣解讀範例1:

var a; // 提升

function a() {
    return 1;
};

// 初始化, 不會被提升
a = 0

console.log(typeof a); // number

https://drive.google.com/uc?export=view&id=12oaRQd-9WaquBjqIhyNXLlAniBSu1ohq

這樣子寫 console 輸出也是 number。

範例2:

// var
var a;
function a() {
  return 1;
};

console.log(typeof a); // function

// let
let a;
function a() {
  return 1;
};

console.log(typeof a); // 重複宣告 let a
                                             // SyntaxError: redeclaration of let a

函式陳述式 (Function Expression)

console.log(typeof aTest); // undefined
aTest(); // function 沒有被提升,出錯了。
         // TypeError: aTest is not a function
var aTest = function hoist() {
    console.log('我被提升了');
};

var aTest 有被提升,但是 function 沒有,aTest 沒有被 initialize,所以是 undefined。

//
// var
aTest(); // function 沒有被提升,出錯了。
         // TypeError: aTest is not a function
var aTest = function() {
    console.log('我被提升了');
};

//
// let
aTest(); // function 沒有被提升,出錯了。
         // ReferenceError: can't access lexical declaration 'aTest' before initialization
let aTest = function() {
    console.log('我被提升了');
};

//
// const
aTest(); // function 沒有被提升,出錯了。
         // ReferenceError: can't access lexical declaration 'aTest' before initialization
const aTest = function() {
    console.log('我被提升了');
};

一樣都是 function,怎麼一個有被提升一個沒有呢?

letconst 宣告 function expression 都會出現很明確的錯誤:ReferenceError: can’t access lexical declaration ‘findProd’ before initialization (暫時性死區 Temporal Dead Zone, TDZ)。

  • 函式提升 > 變數提升
  • assignment (=) 不會被提升
  • Function Expression 不會被提升,function declaration 則會

Reference:
ECMAScript 6 入門: let 和 const 命令
重新認識 JavaScript: Day 10 函式 Functions 的基本概念
我知道你懂 hoisting,可是你了解到多深?
Hoisting in JavaScript

JS Day 37: Var, Let and Const
  • var:宣告變數
  • let :宣告只作用在當前區塊的變數
  • const:宣告常數

var & let

{
  var a = 'A';
  let b = 'B';
    const c = 'C';
}
console.log('var a: ' + a);
console.log('let b: ' + b);
console.log('let c: ' + c);

// output
// "var a: A"
// ReferenceError: b is not defined
// ReferenceError: c is not defined

var 來宣告一個全域變數,let & const 的作用域僅 scope 限於 { ... } 區塊。

作用域 (Scope)

  • var: function { }
  • let: block
// var
function varTest() {
  var x = 1;
  {
    var x = 2; // 覆蓋掉上方的 1
    console.log(x);  // 2
  }
  console.log(x);  // 2
}

// let
function letTest() {
  let x = 1;
  {
    let x = 2;  // 跟上方的 let x = 1; 不相干
    console.log(x);  // 2
  }
  console.log(x);  // 1
}

var 作用範圍是「整個」function。

for loop 作用域

for (let i = 0; i < 5; i++) {
    console.log(i);
};

console.log(i);

// output
// 1
// 2
// 3
// 4
// ReferenceError: i is not defined

let i 在 for loop 裡頭才有計次效用,在 { ... } 外呼叫會得到 ReferenceError 錯誤, i 並沒有被宣告。

for (var i = 0; i < 5; i++) {
  // var 全域
    console.log(i);
};

console.log(i);

// output
// --- LOOP 開始
// 0
// 1
// 2
// 3
// 4
// --- LOOP 結束
// 5   { ... } 外面呼叫 i 得到的結果

一樣意思, 使用 var 的話 setTimeout 取到的 iwindow.ii = 10

for (var i = 0; i < 10; i++) {  
  // var 全域
  console.log('for loop 的 i: ' + i);

  setTimeout( function() {
    console.log('第' + i + '次');
  }, 1000);
}
console.log('i 是 ' + i);

上下兩者的執行結果是一樣的

for (var i = 0; i < 10; i++) {  
  // var 全域
  console.log('for loop 的 i: ' + i);
}
console.log('i 是 ' + i);

setTimeout( function() {
  console.log('第' + i + '次');
}, 1000);

var 改成 let

for (let i = 0; i < 10; i++) {  
  // let 作用域是 block
  console.log('for loop 的 i: ' + i);

  setTimeout( function() {
    console.log('第' + i + '次');
  }, 1000);
}
console.log('i 是 ' + i);

setTimeout 取到的 i 值是 block 裡頭的 i = 9, function scope 外則是 not defined。

switch 作用域

switch (x) {
  case 0:
    let foo;
    break;

  case 1:
    let foo; // SyntaxError for redeclaration.
    break;
}

switch 的作用域為整個 switch (綠色框框),裡面重複宣告 let foo 會出現 Syntax Error: redeclaration of let foo。

重複宣告

var a = 0;
var a = 1;
var a = 2;
console.log(a); // 2

var 可以重複宣告。

let a = 0;
let a = 1;
console.log(a); // SyntaxError: redeclaration of let a

在 switch 作用域 有提到,let 無法重複宣告。

// 那這樣子呢?
var a = 0;
let a = 1;
console.log(a); // SyntaxError: redeclaration of var a

a 已經被宣告過了, let 不允許再次宣告。

const

確定不會重新宣告一個 變數 時,使用 const。

const a = 10;
const name = 'Bob';

// 重新賦予值
a = 20; // TypeError: invalid assignment to const 'a'
name = 'Will';  // TypeError: invalid assignment to const 'name'

一定要賦予值

const 要宣告一個常數,之後不能夠再修改了,所以在一開始一定要賦予值給它。

var a;
let b;
const c;  // SyntaxError: missing = in const declaration

Reference:
ECMAScript 6 入門: let 和 const 命令
重新認識 JavaScript: Day 10 函式 Functions 的基本概念
我知道你懂 hoisting,可是你了解到多深?

JS Day 36: ES6 函式參數的解構賦予值

Function Argument Default 函式參數預設值

在 ES6 之前傳進來的參數必須要透過判斷才可以確定有沒有存在,無法直接設定預設值。現在可以直接在參數後面定義 function checkBMI(bmi = 0) { ... }

範例:

function funcName(foo = 'x', bar = 'y') {
    console.log(foo, bar);
};

funcName('喔')
// => 喔 y

funcName('喔', undefined)
// => 喔 y

funcName('喔', '椰')
// => 喔 椰
// 如果沒有傳入參數,bmi 為 0
function checkBMI(bmi = 0) {
  if (bmi.value === 0) {
    // ... 略過
  }
};

Object Destructuring Assignment 解構賦值

const data = [
  {
    name: '小傑',
    height: 175,
    weight: 70,
    properties: {
      bmi: 22.9,
      state: '理想'
    }
  },
  {
    name: 'Daniel',
    height: 200,
    weight: 68,
    properties: {
      bmi: 17,
      state: '體重過輕'
    }
  },
];

// for...of 迭代陣列
// person = 每筆物件
for (const person of data) {
  renderData(person);
  render(person);
};

// 方法一
function render(person) {
    // 如果層次太多,會變得落落長一串
  console.log('物件: ' + person.name, person.properties.bmi, person.properties.state);
};

// 方法二
// 跟解構變數賦予值方式ㄧ樣
function renderData(
  {
    name, 
    height, 
    weight, 
    properties: { 
      bmi: bmiValue,
      state: stateText
    }
  }) {
  console.log('物件解構: ' + name, bmiValue, stateText); // 變得簡潔
};

解構變數賦予值

const data = {
  name: '小傑',
  height: 175,
  weight: 70,
  properties: {
    bmi: 22.9,
    state: '理想'
  }
};

let { name, height, weight, properties: {bmi: bmiValue, state: stateText} } = data;
console.log(name, bmiValue, stateText);

Reference:
ECMAScript 6 入門
解構賦值

Selenium WebDriver with Ruby on Headless Chrome/Firefox 無頭爬蟲模式

使用 geckodriver 來連接 headless Firefox 與 Selenium 或是使用 chromedriver 來連接 Chrome 與 Selenium

安裝 Geckodriver / Chromedriver

$ brew install geckodriver // Firefox
$ brew cask install chromedriver // Chrome

安裝 Selenium Gem

$ gem install selenium-webdriver

引用 Selenium WebDriver 模組

require 'selenium-webdriver'

指定欲使用的瀏覽器

Firefox

options = Selenium::WebDriver::Firefox::Options.new(args: ['-headless'])
driver = Selenium::WebDriver.for :firefox, options: options

Chrome

options = Selenium::WebDriver::Chrome::Options.new(args: ['headless'])
driver = Selenium::WebDriver.for :chrome, options: options

使用 selenium 操控無頭瀏覽器

# 連結到台灣電子地圖服務網
driver.get('http://www.map.com.tw')

# 點擊搜尋欄位
# <input type="text" style="margin-left:7px;" id="searchWord" value="例如:新北市中和區中正路716號" ondblclick="this.value=''">
driver.find_element(id: 'searchWord').click
# 由於 input 的值設定為'例如:新北市中和區中正路716號',所以必須先清除掉
driver.find_element(id: 'searchWord').clear

# 讓瀏覽器代替輸入'高雄市苓雅區和平一路147號'並送出表單
driver.find_element(id: 'searchWord').send_keys '高雄市苓雅區和平一路147號', :return

# 這邊要等待兩秒, 否則 .winfoIframe 還沒有產生會發生找不到 element
# 當初在這邊卡超久的,幸好有看到花哥分享了 Python 爬蟲有寫到等待,才發現可以這樣用
sleep 2

# store iframe web element
# 另一種方法 iframe = driver.find_element(:css, ".windowInfo > iframe")
iframe = driver.find_element(class: 'winfoIframe')

# 切換到 iframe
driver.switch_to.frame(iframe)

# 點擊 '座標' 圖示進行跳頁
driver.find_elements(class: 'fun')[1].click

# 使用 xpath 取得頁面上的經緯度
coordinates = driver.find_element(xpath: "/html/body/form/div[5]/table/tbody/tr[2]/td").text.split(' ')

# 經緯度
latitude = coordinates[0].split(':')[1]
longitude = coordinates[1].split(':')[1]

puts latitude
puts longitude

Reference:
Geocoding - 批量處理地址轉換經緯度
Selenium

JS Day 34: 中英文字串長度判斷
let eng_string = 'abc'.length; // => 3
let chn_string = '一二三'.length;  // => 3
let mix_string = 'abc一二三'.length // => 6

中文一個字是2個 byte,英文一個字母是一個 byte。要計算一個中英文字串長度的話需要知道中文與英文 unicode 範圍。

漢字 (所有漢字,包含中文簡繁體)

方法

function getLength(str){ 
    console.log(str.replace(/[^\x00-\xff]/g,"OO"));
    return str.replace(/[^\x00-\xff]/g,"OO").length;
}

getLength('abc一二三'); // => 9

這個方法比較簡潔易懂。傳入函式的 str 只要符合非大小寫英文字母與數字的正則 [^x00-xff],都會被 OO 取代,變成 abcOOOOOO。

[\x00-\xff] regular expression 是 0 - 255 的 ASCII, 是 1 byte。 前面的 意思是。表示大於 255 的 ASCII 就是 2 bytes (漢字)。

'abcOOOOOO'.length  // => 9

中文的正則表達式

[\u4E00-\u9FFF] // 中文 Unicode Range

英文正則表達式 (A-Za-z)

[\u0041-\u005A\u0061-\u007A]

Reference:
JavaScript計算含中英文字的字串長度 — 只是個打字的

https://stackoverflow.com/questions/20396456/how-to-do-word-counts-for-a-mixture-of-english-and-chinese-in-javascript — StackOverflow

整理 Unicode 經常會使用到的內碼區域並透過 Regex 自動比對文字 — The Will Will Web

JS Day 28: Array Methods - 修改原始陣列的方法
  • In-place modification (修改原始陣列)
    • copyWithin()
    • fill()
    • pop()
    • push()
    • reverse()
    • shift()
    • unshift()
    • sort()
    • splice()

sort() 排序

sort() 方法會原地(in place)對一個陣列的所有元素進行排序,並回傳此陣列。排序不一定是穩定的(stable)。預設的排序順序是根據字串的 Unicode 編碼位置(code points)而定。 — MDN web docs

驗證:

const array = ['你','我','他'];
array.sort(); 
// => Array(3) [ "他", "你", "我" ]

找出各個元素的 code points

array.map(item => item.codePointAt());
// => Array(3) [ 20182, 20320, 25105 ]

其實 [ "他", "你", "我" ] 就是按照 各個元素的 code points [ 20182, 20320, 25105 ]來排序的。

基本上不大需要這樣的排序方式,所以會傳入一個 comparison function 比較函式來進行排序

arr.sort( compareFunc(a,b) {
    return a - b; // ascending order
});

如果比較函式回傳小於 0 (returns less than zero): a 排在 b 前面

如果比較函式回傳 0 (returns zero): a 與 b 位置保持不變

如果比較函式回傳大於 0 (returns greater than zero): b 排在 a 前面

const nums = [9, 2, 88, 1000, 348];
nums.sort( (a, b) => a - b);
// 結果 => Array(5) [ 2, 9, 88, 348, 1000 ]

執行順序:

  1. a - b

    9 - 2 = 7 (大於 0, b 排在 a 前面)
    nums = [2, 9, 88, 1000, 348];

  2. a - b
    9 - 88 = -79 (小於 0, a 排在 b 前面)
    nums = [2, 9, 88, 1000, 348];

  3. a - b
    88 - 1000 = -912 (小於 0, a 排在 b 前面)
    nums = [2, 9, 88, 1000, 348];

  4. a - b
    1000 - 348 = 652 (大於 0, b 排在 a 前面)
    nums = [2, 9, 88, 348, 1000];

排序:

const ascendingOrder  = arr.sort( () => a - b); // 升序
const descendingOrder = arr.sort( () => b - a); // 降序

練習 (Sorting object array):

JavaScript30 - 04 Array Cardio Day 1

JS 28

References:

[JavaScript] 從 Array 的 sort 方法,聊到各瀏覽器的實作,沒想到 Chrome 和FireFox 的排序如此不同

splice() 刪除/加入

splice() 方法可以藉由刪除既有元素並/或加入新元素來改變一個陣列的內容。 — MDN web docs

array.splice( 起始索引, 欲刪除元素數量, 欲增加的元素)

範例:

// 增加 1 個元素,刪除 0 個元素

let fruits = ['酪梨','桃子','香蕉','葡萄','西瓜'];
fruits.splice(3, 0, '番茄'); 
// output => [ "酪梨", "桃子", "香蕉", "番茄", "葡萄", "西瓜" ]

   0      1     2     3     4
['酪梨','桃子','香蕉','葡萄','西瓜'] 
// 在第 3 個位置加入 '番茄'

   0      1     2     + 3 +     4      5
['酪梨','桃子','香蕉', '[番茄]', '葡萄','西瓜'] 

// 增加 0 個元素,刪除 2 個元素

 let fruits = ['酪梨','桃子','香蕉','葡萄','西瓜'];
fruits.splice(2, 2);  // 從索引 2 開始,刪除兩個元素 [ "香蕉", "葡萄" ]
// output => ['酪梨','桃子','西瓜']

// 增加 4 個元素,刪除 4 個元素

let fruits = ['酪梨','桃子','香蕉','葡萄','西瓜'];
fruits.splice(1, 4, '九層塔', '花椰菜', '紅蘿蔔', '高麗菜');
// 從索引 1 算起,刪除四個元素 [ "桃子", "香蕉", "葡萄", "西瓜" ]
// output => [ "酪梨", "九層塔", "花椰菜", "紅蘿蔔", "高麗菜" ]

練習:

JS 28 (Terry Yu) - 把 id 206 的物件移動到陣列索引 0(第一個)

const terryElement = data.find(p => p.id == 206); // return 物件
const terryIndex = data.findIndex(p => p.id == 206); // return 索引
data.splice(terryIndex, 1); // 從索引x刪除一個元素
data.splice(0, 0, terryElement); // 從索引 0 加入 terryElement 並刪除 0 個元素

unshift(), shift(), push(), pop() 這四個算是比較好理解的,但是名稱取得很容易讓人搞混(除了 push)。下方的連結都在講解 stack and queue 可以更深入了解這四個方法。

JavaScript Arrays as Queue and Stack
Stacks and Queues, Simplified
堆疊(Stack) & 佇列(Queue)
你其實不用在 JavaScript 實作一個 Queue

unshift() 方法會添加一個或多個元素至陣列的開頭,並且回傳陣列的新長度。 — MDN web docs

// unshift()
let arr = [1, 3, 5, 7, 9]
arr.unshift('你', '我', '他');  // 新增 3 個元素到陣列前

unshift 被刻意設計為具通用性;此方法可以藉由 called 或 applied 應用於類似陣列的物件上。若欲應用此方法的物件不包含代表一系列啟始為零之數字屬性序列長度的 length 屬性,可能是不具任何意義的行為。 — MDN web docs

MDN 這部分的描述不太明白,之後會再補上。

shift() 移除第一個元素

shift() 方法會移除並回傳陣列的第一個元素。此方法會改變陣列的長度。 — MDN web docs

// shift()
let fruits = ['香蕉', '酪梨', '鳳梨', '葡萄'];
fruits.shift(); // 移除第一個元素 ”香蕉“

fruits.shift(2); // 參數 2 沒有作用,只有第一個元素會被移除
console.log(fruits); // => ["鳳梨", "葡萄"]

Push() 在末端加入元素

push() 方法會添加一個或多個元素至陣列的末端,並且回傳陣列的新長度。 — MDN web docs

// push()
let colors = ['red', 'pink', 'orange'];

colors.push(); // 沒有作用,陣列長度維持一樣
console.log(colors.length);
colors.push('yellow'); // 新增 'yellow' 在末端
console.log(colors.length);
colors.push(['black','white']); // 傳入一個陣列在末端
console.log(colors);

Pop() 在末端移除元素

pop() 方法會移除並回傳陣列的最後一個元素。此方法會改變陣列的長度。 — MDN web doc

// pop()
let fruits2 = ['香蕉', '酪梨', '鳳梨', '葡萄'];
fruits2.pop(); // 移除最後一個元素 '葡萄'

fruits2.pop(2); // 參數 2 沒有作用,但最後一個元素會被移除
console.log(fruits2); // => ["香蕉", "酪梨"]

CodePen.io: unshift(), shift(), push(), pop()

copyWithin() 陣列內複製元素

copyWithin() 方法會對陣列的一部分進行淺拷貝(shallow copies)至同一陣列的另一位置並回傳此陣列,而不修改其大小。 — MDN web doc

將一個陣列裡的某一個元素複製到同陣列的某一索引並覆蓋掉。

// copyWithin(終點索引, 被複製的索引, 如沒有輸入會複製到最後);
let foods = ['🍞', '🍔', '🍟', '🍕', '🍙', '🍳', '🍦'];
foods.copyWithin(1, 5); 
// 從索引 5 到最後一個元素 🍳, 🍦
// 覆蓋掉索引 1 & 2 🍔, 🍟
// ['🍞', '🍳', '🍦', '🍕', '🍙', '🍳', '🍦'];

let foods2 = ['🍞', '🍔', '🍟', '🍕', '🍙', '🍳', '🍦'];
foods.copyWithin(0, 4, 5); // 複製索引 4 到 5 個元素 🍙,🍳 到索引 0

fill() 在陣列裡指定的位置覆蓋上靜態值

// fill()
let phil = [1, 2, 3, 4];
phil.fill('i', 1, 3) // 從索引1到索引2 (不包含第3)
// => [1, 'i', 'i', 4]

reverse() 把陣列順序顛倒

// reverse()
let num = ['一', '二', '三'];
num.reverse(); // => ['三', '二', '一'];

References:

The Modern JavaScript Bootcamp (2020)

MDN web docs

JS Day 30: C3.js Chart

目標

運用 axios 把 JSON 資料載入,再使用 C3.js graph 把數據呈現出來

練習

https://codepen.io/william_k/pen/LYGXLeV

載入 C3.js & CSS Style

<!-- CSS Style Link -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.18/c3.min.css" integrity="sha512-cznfNokevSG7QPA5dZepud8taylLdvgr0lDqw/FEZIhluFsSwyvS81CMnRdrNSKwbsmc43LtRd2/WMQV+Z85AQ==" crossorigin="anonymous" />

<!-- JavaScript Link -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js" integrity="sha512-FHsFVKQ/T1KWJDGSbrUhTJyS1ph3eRrxI228ND0EGaEp6v4a/vGwPWd3Dtd/+9cI7ccofZvl/wulICEurHN1pg==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.18/c3.min.js" integrity="sha512-bW79RVtvrrTS2QzmDsJeh62Nt4b/RjYlYvi2iEmkXPhzzbXMx69JT/zHgiGcL1Tk5nkLMTF6xkEUuynTkdC9PQ==" crossorigin="anonymous"></script>

元素綁定

在 C3.JS 官網裡的 Getting Started 說明的很清楚,要生成一個圖表需要提供一個元素給圖表綁定。

<div id="chart"></div>

產生圖表

#chart_donut.js

var chart = c3.generate({
    data: {
                // columns 是要存放排列好的陣列
                // 需要把 axios 取得的 response.data 變成這樣
        columns: [['data1', 30],['data2', 120],],
        type : 'donut',
        onclick: function (d, i) { console.log("onclick", d, i); },
        onmouseover: function (d, i) { console.log("onmouseover", d, i); },
        onmouseout: function (d, i) { console.log("onmouseout", d, i); }
    },
    donut: {
        title: "Iris Petal Width"
    }
});

https://c3js.org/samples/chart_donut.html

C3.js 官網裡面每一個範例下方都有內建編輯器,非常好用。可以直接把我們的資料丟進去,馬上可以看到成果 👍。

JS%20Day%2030%20C3%20js%20Chart%20596ae2ec69c04c08b18a60b77394d6dd/Screen_Shot_2020-07-18_at_11.04.51_PM.png

原始資料

從 JSON 取出來的資料原始面貌如下:

// Array (213)
[
    {id: "4", name: "hsin-yu", process: "45%", checkpoint: {…}},
    {id: "9", name: "Sesame", process: "45%", checkpoint: {…}},
    .... 略
    {id: "227", name: "skypassion5000", process: "0%", checkpoint: {…}}
]
// 要變成下面這個結構需要去計算每一個%出現的次數。
let chartData = [['0%', 5], ['5%', 3], ['10%', 15], ['15%', 11]];

方法其實有很多種,forEach(), map(), reduce() 都可以做到。這邊選擇使用 reduce(),比較簡單。

reduce() 方法將一個累加器及陣列中每項元素(由左至右)傳入回呼函式,將陣列化為單一值。 — MDN web docs

JS%20Day%2030%20C3%20js%20Chart%20596ae2ec69c04c08b18a60b77394d6dd/Screen_Shot_2020-07-18_at_11.18.06_PM.png

// reduce() - 計算相同元素數量並以物件鍵值顯示
let chartObj = {};

function progressCollection() {
  chartObj = data.reduce( (obj, item) => {
    if (!obj[item.process]){  // 如果 value (例:'15%') 不存在
      obj[item.process] = 0;  // 新增 {'15%':0}
    }
    obj[item.process]++;  // 把找到的+1  {'15%':1}
    return obj;
  }, {});  // 起始值是一個空物件 {} arg= obj
}

// chartObj output
{
    0%: 59, 1.65%: 1,
    5%: 12,
    8.3%: 1,
    10%: 9,
    11.65%: 1,
    15%: 19,
    16.65%: 1,
    20%: 21,
    21.65%: 1,
    23.25%: 1,
    25%: 10,
    28.3%: 1,
    30%: 11,
    35%: 4,
    39.95%: 1,
    40%: 18,
    41.65%: 1,
    43.3%: 1,
    45%: 40
}

距離目標還差一小步,把物件轉成陣列,幸好 JS 原生 API 就有提供轉換

// MDN example
const obj = { foo: 'bar', baz: 42 };
console.log(Object.entries(obj)); 
// [ ['foo', 'bar'], ['baz', 42] ]


// 方法二
Object.keys(chartObj).map((key) => [key, chartObj[key]]);
// 這樣就可以把 chartObj 物件轉換成陣列了
Object.entries(chartObj);

// output
[
  ["45%", 40],
  ["43.3%", 1],
  ["41.65%", 1],
  ["40%", 18],
  ["39.95%", 1],
  ["35%", 4],
  ["30%", 11],
  ["28.3%", 1],
  ["25%", 10],
  ["23.25%", 1],
  ["21.65%", 1],
  ["20%", 21],
  ["16.65%", 1],
  ["15%", 19],
  ["11.65%", 1],
  ["10%", 9],
  ["8.3%", 1],
  ["5%", 12],
  ["1.65%", 1],
  ["0%", 59]
 ]

互動性

setTimeout(function () {
  var chart = c3.generate({
    data: {
      columns: pieColumns,
      type: 'donut',
      onclick: function (d, i) {

        const selected = document.querySelectorAll('#table_body > tr > th')

        selected.forEach( (item, index) => {
          item.parentElement.className = '';
          if (item.innerText === d.id){
            item.parentElement.classList.add('table-dark');
          }
        })
      },
      // onmouseover: function (d, i) { console.log("onmouseover", d, i); },
      // onmouseout: function (d, i) { console.log("onmouseout", d, i); }
    },
    donut: {
      title: "JS學徒試煉"
    }
  });
}, 1000);

C3 donut chart 本身內建了一些事件,可以透過它來增強互動性。

當使用者點擊 donus chart 某個部份,下方 table row 也會選擇那幾個row

References:

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

$ hexo new "My New Post"

More info: Writing

Run server

$ hexo server

More info: Server

Generate static files

$ hexo generate

More info: Generating

Deploy to remote sites

$ hexo deploy

More info: Deployment