2018年9月11日 星期二

三種方式產生 scatter plot / bubble chart

太陽系較大的一些衛星的軌道半長軸及公轉週期 資料視覺化的 常用圖形類別 當中, 我最喜歡用 scatter plot (散點圖) 以及它的變形 bubble chart (氣泡圖), 因為 scatter plot 能用位置 (X-Y 座標) 及顏色把一張試算表的兩個數值欄位及一個類別欄位同時呈現在一張圖上; 而 bubble chart 則再補上 「圓圈大小」, 較 scatter plot 更多展現出一個數值欄位。 為了畫 bubble chart, 暑假我繞了好多遠路。 最後找到最簡單的方法, 把一些重點提示摘要在這篇裡。 從這篇快速起步之後, 每一種方法當然都還有更多本文未提及的參數可以調整, 請自行搜尋/查手冊。 程式碼及資料檔 (satellites.csv) 放在 github 的 ckhung/scatplot; 範例圖的物理意義請見 重新發現克卜勒第三定律

一、 gnuplot

這是最簡單的方法: 完全不需要寫程式, 只要用 gnuplot, 幾個指令就把圖畫出來了:

set datafile separator ","
set encoding utf8
set logscale
plot "satellites.csv" using 3:4:(log($2))*0.8 with points lt 1 pt 6 ps variable, "satellites.csv" using 3:4:1 with labels
set term svg size 1024,768
set output satellites.svg
replot

重點提示:

  1. gnuplot 預設以空格區分資料檔的欄位。 如果要讀的資料檔是 csv 格式, 則需要先 set datafile separator ","
  2. set encoding utf8 是為了正確顯示中文。
  3. 真正繪圖的 plot 指令可以一次畫好幾樣東西。 像上面就是用大小不同的圈圈畫一次 (with points ... pointsize variable) satellites.csv 檔, 再用文字 (with labels) 把同一個檔重畫一次。
  4. "using" 後面放欄位代號。 本例當中, 兩次都以第三欄 (orbit_major) 為 X 軸、 第四欄 (rev_cycle) 為 Y 軸畫圖。
  5. 以大小不同的圈圈畫圖時, using 後面的 「A:B:C」 第三段所指定的欄位決定圈圈的大小。 若用小括弧括起來, 則可以寫數學運算式; 但此時要改用 $2 來表示 csv 檔的第二欄數值。 這裡為了減少 「衛星半徑大小」 之間的巨大差異, 所以連圈圈的大小也採用半徑的 log 值來決定。
  6. 最後兩句話可以將圖存成 svg 檔。 但做完之後就應按 ^d 離開 gnuplot, 否則之後再下的畫圖指令都會畫到這個 svg 檔裡面去。

二、 python

以 python 撰寫 scatter plot / bubble chart 程式, 比 gnuplot 多了很多彈性。 先試用一下我寫的程式: ./scatplot.py -t 'name planet' -k 'radius>100' -X 'math.log(orbit_major,10)' -Y 'math.log(rev_cycle,10)' -A 'radius' -T 'name[:3]' satellites.csv 這會 (根據輸入的 csv 檔名, 含完整路徑) 產生一個 satellites.svg 圖檔。 首先你必須用 -t 告訴它哪幾個欄位是文字欄位; 沒列出來的一律視為數字欄位。 再來用 -k 告訴它要以什麼條件只保留部分資料列。 這可以是任意的 python 數學/邏輯運算式。 -X 跟 -Y 指定以哪些欄位決定圈圈的位置; -A 決定決定圈圈的大小; -T 決定圈圈內的文字。 以上幾個選項都可以用數學/文字運算式, 而不限於單純的欄位名稱。

scatplot.py 採用 matplotlib 繪圖函式庫。 以下是我對 matplotlib 的心得/提示:

  1. 簡中入門教學文; 不過我其實沒有讀, 都是邊畫邊搜尋胡亂學。
  2. 先解決 中文顯示問題
  3. 讀取 csv 檔: 原先一直卡在中文; 直到自己刻完簡單版, 才發現 python3 的 csv 讀取函數直接可處理 utf8
  4. python 的 eval 很好用, 可以讓用戶輸入運算式, 大大提升程式碼的彈性。 但是要小心安全性, 以免用戶輸入 'os.system("rm -rf /")' 之類的惡意算式。 參考 eval in Python 再按照 這個問答修改, 對 eval 設限, 只允許輸入數學函數及欄位名稱所組成的運算式。 危險性也許還是存在; 不過我的範例程式並非用於高資安需求環境, 所以這麼複雜、 看不懂的攻擊, 就不處理了。
  5. 最關鍵的畫圖函數是 matplotlib.pyplot.scatter。 如果不想畫圓圈, 可以用 marker 參數指定其他圖形, 甚至用 verts 參數畫自訂的圖形。 在指定的 X-Y 座標寫字則用 matplotlib.pyplot.text; 但是 axis autoscaling 機制 (根據繪圖內容調整 X-Y 軸範圍) 忽略了圖裡面的文字, 所以補上一條看不見 (linestyle='') 的對角線, 確保圖形範圍正確。
  6. 先前不知道有 scatter() 可用時, 想要 用 artists 跟 transform 處理圓圈變成橢圓的問題, 越改越複雜, 甚至遇到 這個奇怪的 bug, 總之寫出一個超醜的程式。 還好後來改用 scatter() 程式就變得漂亮、 簡單多了。

三、 javascript

用 javascript 寫資料視覺化的優點是: 使用者可以在瀏覽器裡直接放大縮小圖形。 如果選對了函式庫, 甚至還可以有許多其他互動方式。 缺點當然就是門檻比 python 高很多。 從 Compare the Best Javascript Chart Libraries 這篇文章出發, 拿各家函式庫的名字跟 "scatter plot" 或 "bubble chart" 搜尋, 最後決定用 plotly。 它的使用方式超簡單: 把資料準備好、 參數設定好, 就幫你把圖畫出來, 還附贈一大堆使用者互動工具。

請見我 2020 年寫的程式與解說: scatplot: 一張試算表, 散點圖畫到飽 其中包含 太陽系天然衛星軌道常數

以下是關於程式碼的一些心得及說明:

  1. 建議先拿 入門範例 bubble charts 範例 來改寫出你自己的第一個範例程式, 然後才來讀我的程式。
  2. 關於讀取 config.json 及 csv 檔, 請參考 用 jQuery 無痛讀檔、顯示
  3. 開發過程當中, 有時瀏覽器會 cache 住舊的資料檔, 導致更新資料檔之後, 即使重新整理網頁, 還是看到舊的圖。 根據 這個問答, 在檔名後面加上一串亂數字串即可強迫每次都更新。
  4. 讀取 csv 檔是很常見的工作, 應該有現成的函式庫對吧? 但我忘記找到哪裡去了, 總之遇到 手機上的瀏覽器不支援某些函數 的問題。 後來乾脆自己刻一個簡單版比較快。
  5. 自己寫過範例程式之後就會同意: plotly 真的超好用的; 你的時間多半只需要花在整理資料上面 -- 把 X-Y 座標、 圓圈的大小、 圓圈外框顏色分別放到正確的欄位去 (x、 y、 marker.size、 marker.line.color)。 所以我寫了 gentrace() 函數專門做這件事, 裡面主要用到一大堆 .map() 函數在整理資料。
  6. 想要讓圓圈變成半透明的, 請參考 這篇
  7. 當滑鼠移到圓圈上時, 希望顯示文字, 要參考 這篇
  8. 最後順便拿 (專門顯示表格的) 好用 jQuery 外掛 DataTables 來呈現原始的 csv 資料。

四、 結語

即使沒想要改程式, 光是拿我這兩個小程式去用, 都已經可以畫出一些有趣的圖了喔! 有哪些數字表格可以拿來畫 bubble chart 呢? 請留言分享吧!

1 則留言:

  1. 這種中階段 (時期) 的應用,可能是學程式設計開始變得有趣的時候。前期看不到應用和盡頭一片黑暗,是最枯燥無聊沒有成就感最容易放棄的階段。
    但再往下學下去,可能又開始無聊了,因為寫的東西不是 disposable (一次性) 的,要花很多時間維護,要想怎樣寫才會漂亮簡潔好維護,要想這個想那個的,不是很自由不是很隨性。

    回覆刪除

因為垃圾留言太多,現在改為審核後才發佈,請耐心等候一兩天。