背景

之前工作上有需要對做 hierarchical clustering 結果做視覺化的 task。做資料處理以及建模這塊是使用 Python 相關的工具但 Python 的 ecosystem 似乎沒有可以像 d3-hierarchy 一樣提供互動性以及這麼多客製化選項的工具。而且考慮呈現方式時,也是使用 D3.js 這類工具整合網頁對 stakeholder 來說會比較方便。

在開發階段的問題主要就是我們在 JupyterLab 上面做調整,而 JupyterLab 現在並沒有支援可以呈現 D3.js 結果的 extension ( 在 Jupyter Notebook 上有一個,但目前看起來沒有要移植到 JupyterLab 的計畫 )。造成開發上一些小不便。

這篇文章會用 Observable 上的這個 Collapsible Tree 當作例子,試著改寫裡面內容讓它可以在 JupyterLab 裡面使用。

在 JupyterLab 裡面執行 JavaScript

JupyterLab 本身就是 JavaScript 或者是說 TypeScript 所寫成,IPython 內建的 magic 裡面有支援javascript magic,可以直接在 Notebook 裡面執行 JavaScript 的程式碼。也可以透過 IPython 的 API 使用 Python 執行含有 JavaScript 程式碼的字串:

from IPython.display import Javascript
Javascript(
    'console.log("D3 is awesome!")'
)

執行完上面這段程式碼後可以在瀏覽器的 Console 中看到 "D3 is awesome!" 這段 log。

要用 D3.js 以及 d3-hierarchy 前我們需要載入他們的 script,這邊可以透過 html magic 來完成:

%%html
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://d3js.org/d3-hierarchy.v2.min.js"></script>

改寫 Observable 的語法

Observable 有支援一些特殊的語法,而我們要將 Observable 上特別的寫法成普通的 D3 JavaScript。

// 將 Observable notebook 最底下跟大小跟 style 有關的變數拿過來
const dx = 10;
const width = 1080; // 設一個適合你環境的寬度
const dy = width / 6;
const margin = ({top: 10, right: 120, bottom: 10, left: 40})
const tree = d3.tree().nodeSize([dx, dy]);

後面會把這部分跟 Observable 裡面 chart 的部分擺在一起來使用。

選取 JupyterLab 中的 cell 輸出 ( 修改 chart 裡面的 d3.select)

例子裡面 chart 的部分是會回傳 svg.node() 當作輸出,而正常使用 D3.js 的時候,通常會加一個 svg 的元素到 HTML 中,在 JupyterLab 裡面,我們希望是加到 cell 的 output 裡面,而 JupyterLab 每個 cell 會用一個 element 變數表示當前 output 的 DOM,所以我們需要用 d3.select 選取這個 DOM 並且加我們要輸出的 svg 到裡面去:

// 省略其餘的 JavaScript...
const svg = d3.select(element)
    .append("svg")
// 省略其餘的 JavaScript...

chart 除了最後面 return svg.node() 需要拿掉,剩下部份就直接沿用即可。

將 Python 的資料傳給 JavaScript 來使用

這個例子裡面資料 (flare-2.json) 已經是 D3 可以接受的 hierarchical 格式,如果大家的資料不是這種格式,請參照 d3.hierarchy 來修改。我們這邊用 Python 的 library 來讀這個 json:

# 假設檔案在你的 CWD
with open('./flare-2.json') as f:
    data_str = f.read()

這邊我們用一個簡單明瞭的方式把值傳到 JavaScript 裡面。例子裡面用const root = d3.hierarchy(data)的方式傳入資料,我們將 data 這個變數直接用上面讀好的 json 字串換成 $data ,然後把將上面幾個步驟處理完的 JavaScript 一起傳入 Python stdlib 中的 Template

tree_js_template = Template("""
// 省略其餘的 JavaScript...
const root = d3.hierarchy($data);
// 省略其餘的 JavaScript...
"""
)

接下來只要把 Template 中的 $data 替換成真正的 data,傳到 IPython 的 JavaScript 函式就大功告成了:

JavaScript(tree_js_template.safe_substitute(data=data_str))

完整範例

完整的程式碼可以參考這裡

總結

這邊很簡單地將一個 Observable 上的例子改寫,如果你的 JavaScript 程式碼比較複雜,可以考慮獨立寫成一個檔案來使用,也可以試功能比齊全的 template library 像是Jinja。看得懂日文的讀者可以參考這篇

Comments

comments powered by Disqus

Published

Category

Data Visualization

Tags

Contact