mkdocsにbokehを埋め込む
静的サイトジェネレータの mkdocs に bokeh のグラフを埋め込む方法を調べた。 端的にいうと、bokeh の html ファイルの script タグなどを、Markdown ファイルに直書きすることで埋め込める。 他の静的サイトジェネレータの場合は html ファイルのまま include する手段があるようだけど、mkdocs の場合はなさそうだったので、タグを直書きする方法を用いた。
bokeh のグラフを script タグなどのパーツとして取り出す方法はいくつかあるようだけど、以下、2つの方法についてメモしておく。
方法1:bokeh.embed.components を使う方法
この方法だと、script タグと、canvas が埋め込まれる div タグとに分けて取り出すことができる。
以下のコードでは、script タグについてはスクリプトの部分だけを取り出すために、wrap_script という引数の値を False にしている。
from bokeh.plotting import figure from bokeh.embed import components # プロット p = figure() p.circle([1, 2, 3, 4, 5], [6, 7, 2, 4, 5], size=20, color='navy', alpha=0.5) # パーツの生成 script, div = components(p, wrap_script=False) # ファイルとして出力 with open('sample_1.js', mode='w') as f: f.write(script) with open('sample_1.html', mode='w') as f: f.write(div)
div 要素が書き込まれる sample_1.html の中身は、以下のようになる。
<div class="bk-root" id="5896505b-7646-4192-b221-4d0ba6db2f5d" data-root-id="1001"></div>
一方、script 要素が書き込まれる sample_1.js の中には、グラフのデータと js のコードが入る。 この js ファイルを呼び出す script タグを、下記のように自分で書いておく。
<script src="sample_1.js"></script>
以上のタグと、bokeh のライブラリを CDN から読み込む script タグとを合わせて、下記のような Markdown ファイルを作成する。
## Bokeh test <script src="https://cdn.bokeh.org/bokeh/release/bokeh-2.1.1.min.js" crossorigin="anonymous"></script> <div class="bk-root" id="5896505b-7646-4192-b221-4d0ba6db2f5d" data-root-id="1001"></div> <script src="sample_1.js"></script>
これを mkdocs でビルドすると、グラフがちゃんと埋め込まれる。もちろん、インタラクティブに動かすこともできる。 bokeh のライブラリをロードする部分は、mkdocs.yml の extra_javascript の項に書くこともできる。
site_name: My Docs extra_javascript: - https://cdn.bokeh.org/bokeh/release/bokeh-2.1.1.min.js
方法2:bokeh.embed.autoload_static を使う方法
次に、autoload_static を使う方法。この方法を使うと、div と script タグをひとつにまとめて取り出すことができる。 さらに、CDN から bokeh のライブラリをロードする処理を含めることもできる。
from bokeh.plotting import figure from bokeh.resources import Resources from bokeh.embed import autoload_static # プロット p = figure() p.circle([1, 2, 3, 4, 5], [6, 7, 2, 4, 5], size=20, color='navy', alpha=0.5) # パーツの生成 resources = Resources(components=['bokeh']) js, tag = autoload_static(p, resources, 'sample_2.js') # ファイルとして出力 with open('sample_2.js', mode='w', encoding='utf-8') as f: f.write(js) with open('sample_2.html', mode='w') as f: f.write(tag)
components という引数の値を、空のリスト [] にすると、CDN からライブラリをロードする処理は行われない。 デフォルトの値は None であるが、その場合、ウィジット等を含む4種類のライブラリすべてがロードされてしまうので、ここでは ['bokeh'] という値のみを設定している。*1
sample_2.html の中身は、以下のようになっている。
<script src="sample_2.js" id="2cb55733-863c-467c-a61b-8da7e404f74d"></script>
これを Markdown ファイルに書き込んで、sample_2.js を適切な場所に配置すれば、ひとつ目の方法同様に bokeh のグラフが mkdocs に埋め込まれる。
以上おわり。
感想
mkdocs も bokeh も最近使い始めたので、本当はもっとスマートな方法があるのかもしれない。 というかそれ以前に、autoload_static とか他の関数とかも、イレギュラーな使い方をしているのかもしれない。
資料
- bokeh.embed.components
https://docs.bokeh.org/en/latest/docs/user_guide/embed.html#components - bokeh.embed.autoload_static
https://docs.bokeh.org/en/latest/docs/user_guide/embed.html#autoload-scripts
*1:他の3つは、'bokeh-widgets', 'bokeh-tables', 'bokeh-gl' という値をとる
Lightweight chartsでロウソク足を描画
課題
- Tradingview が作っている lightweight-charts というライブラリを使ってみたい。
- インジケータとかは今は不要なので、とりあえずロウソク足だけの、動くグラフを作りたい。
- 使うデータは websocket で取得して、それをロウソク足に加工してプロットするようにしたい。
使い方の概要
使い方を調べたので、その概要をメモしておく。
- chart オブジェクトなるものを、LightweightCharts の createChart メソッドを呼び出して作る。
- chart オブジェクトのメソッドを使って、グラフの基になる series オブジェクトなるものを作る。
- series オブジェクトの update メソッドなどを使って、series にデータを入れていく。
- ブラウザを覗いてみると、グラフが出現している。
実際には1の段階でチャートの枠だけはブラウザに出現していて、series にデータを入れ始めるとグラフが出現するようになっている。
できたチャート
意外と簡単に作れたので驚きだったんだけど、オブジェクトを生成するときのパラメータの設定部分でハマった。ただ各パラメータの意味はドキュメントにちゃんと載っているので、そこはかなり助けられた。ドキュメントってやっぱり大事。*1
コード
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>Lightweight Charts v3</title> <script src="https://unpkg.com/lightweight-charts@3.x.x/dist/lightweight-charts.standalone.production.js"></script> </head> <body style="margin:0"> <script> { // Setup chart const chart = LightweightCharts.createChart(document.body, { width: window.innerWidth, height: window.innerHeight, layout: {backgroundColor: "#131722", textColor: "#ccc"}, crosshair: {mode: LightweightCharts.CrosshairMode.Normal}, timeScale: {rightOffset: 10, timeVisible: true}, priceScale: {entireTextOnly: true, scaleMargins: {top: 0.1, bottom: 0.22}}, grid: { vertLines: {color: "#363c4e", style: LightweightCharts.LineStyle.Dashed}, horzLines: {color: "#363c4e", style: LightweightCharts.LineStyle.Dashed}, }, }); // Create series const candle = chart.addCandlestickSeries({ priceFormat: {type: 'custom', formatter: price => Math.floor(price)}, }); const volume = chart.addHistogramSeries({ priceFormat: {precision: 3}, priceScaleId: '', scaleMargins: {top: 0.8, bottom: 0}, }); // Update series function updateChart() { candle.update({ time: ohlc[0], open: ohlc[1], high: ohlc[2], low: ohlc[3], close: ohlc[4], }); volume.update({ time: ohlc[0], value: ohlc[5], color: (ohlc[1] > ohlc[4]) ? "#ef5350" : "#26a69a", }); } // Settings const SECOND_PERIOD = 1; // Internal data let ohlc = []; let next_time = 0; // Update internal data function updateOhlc(msg) { const exec_time = Math.floor(new Date(msg.exec_date).getTime() / 1000); if (!next_time) { ohlc = [exec_time, msg.price, msg.price, msg.price, msg.price, msg.size]; next_time = exec_time + SECOND_PERIOD; } else if (exec_time < next_time) { ohlc[2] = (ohlc[2] > msg.price) ? ohlc[2] : msg.price; ohlc[3] = (ohlc[3] < msg.price) ? ohlc[3] : msg.price; ohlc[4] = msg.price; ohlc[5] += msg.size; } else if (exec_time < next_time + SECOND_PERIOD) { ohlc = [next_time, msg.price, msg.price, msg.price, msg.price, msg.size]; next_time += SECOND_PERIOD; } else { ohlc = [next_time, ohlc[4], ohlc[4], ohlc[4], ohlc[4], 0]; updateChart(); next_time += SECOND_PERIOD; updateOhlc(msg); } updateChart(); } // Websocket const ws = new WebSocket("wss://ws.lightstream.bitflyer.com/json-rpc"); ws.onopen = function(event) { ws.send(JSON.stringify({ method: "subscribe", params: {channel: "lightning_executions_FX_BTC_JPY"} })); }; ws.onmessage = function(event) { JSON.parse(event.data).params.message.forEach(msg => updateOhlc(msg)); }; // Resize window window.onresize = () => chart.resize(window.innerWidth, window.innerHeight); } </script> </body> </html>
※ Websocket が切れた時の再接続はしていないので、接続が切れると描画が止まる。
※ 上記を少し改変したものを GitLab Pages に上げてみた。
https://bitbot125.gitlab.io/lightweight_charts_test/
感想
TradingView 社製だけあって動作が軽くて CPU にも優しい。といっても半日動かしていると(series.updateし続けていると)少しだけ重たくなってくる。都度、古いデータを削除すればいいのかもしれないが分からない。