Jupyter NotebookでJavaScriptを使ってデータを可視化する

最初はPythonで作業していたのだが、言語特性上pandasなりnumpyなりのデータセットでデータを取り扱わないと恐ろしく遅い。最終的にはNodeでツール化することを検討していたので、分析もJavaScriptでやってみようと思った。

ijavascript

Jupyter NotebookJavaScript用カーネル

インストール方法

Anacondaで環境構築していたので、公式サイトに記載されている通り、Anaconda promptで以下を実行するだけでOKだった。

conda install nodejs
npm install -g ijavascript
ijsinstall

可視化方法

使ってみたところ、データ操作は問題ないのだが、データの可視化方法がわからず困った。
公式サイトを見ると、NotebookOutputエリアに、任意のHTMLを埋め込む仕組みがあった。

$$.html("<div style='background-color:olive;width:50px;height:50px'></div>");

こんな感じで記載すると、記載したHTMLがOutputエリアに描画される。

可視化ライブラリハ状況に応じていろいろなものを使いたくなることが想定されるため、CDNで提供されているものが使える方法を検討し、以下のような感じで実装した。

  1. Webサイトでグラフを描画するのと同じコードでHTMLを記載しグラフを描画する
  2. ライブラリの読み込みやDOM要素をNotebookから独立させるためiframe内にHTMLを記載する
  3. シリアライズした描画データを上記HTMLに埋め込むことで、Notebook上のデータをHTMLへ連携する

Chart.js

共通関数

Chart.jsを使うならこんな感じ

function chartjs(config, width='100%', height=500) {
    var html =  `<iframe width='${width}' height='${height}' srcdoc="
        <head><script src='https://cdn.jsdelivr.net/npm/chart.js@2.8.0'></script></head>
        <body>
        <canvas id='canvas'></canvas>
        <script>
            window.onload = function() {
                var ctx = document.getElementById('canvas').getContext('2d');
                window.myLine = new Chart(ctx, ${JSON.stringify(config).replace(/"/gi,'\'')});
            }
        </script>
        </body>
    ">`;
    $$.html(html)
}
グラフ描画

configChart.jsのサンプルそのまま。これを先程作ったchartjs関数に渡している。これだけでOutputエリアにグラフを描画することができる。

var config = {
    type: 'bar',
    data: {
        labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
        datasets: [{
            label: '# of Votes',
            data: [12, 19, 3, 5, 2, 3],
            backgroundColor: [
                'rgba(255, 99, 132, 0.2)',
                'rgba(54, 162, 235, 0.2)',
                'rgba(255, 206, 86, 0.2)',
                'rgba(75, 192, 192, 0.2)',
                'rgba(153, 102, 255, 0.2)',
                'rgba(255, 159, 64, 0.2)'
            ],
            borderColor: [
                'rgba(255, 99, 132, 1)',
                'rgba(54, 162, 235, 1)',
                'rgba(255, 206, 86, 1)',
                'rgba(75, 192, 192, 1)',
                'rgba(153, 102, 255, 1)',
                'rgba(255, 159, 64, 1)'
            ],
            borderWidth: 1
        }]
    },
    options: {
        scales: {
            yAxes: [{
                ticks: {
                    beginAtZero: true
                }
            }]
        }
    }
}

chartjs(config); 

Google Charts

少し応用して、bitFlyerの取引データを、Google Chartsを使ってローソク足チャートとして表示する。
グラフのオプションはJavaScriptでローソク足チャートの作成を参考にさせていただきました。

メイン処理

bitflyer APIでの取引情報取得は1回で最大500件までしか取得できない。500件程度では延べ時間にして1分程度にしかならないので、任意のデータ量まで溜めてから可視化するよう、規定数まで取引データが溜まっていなければfetchApiを再帰呼び出しするようにしている。(下記サンプルでは3000件)
規定数まで溜まっていれば、データ加工パラメータ生成グラフ描画を行う。

var fetch = require("node-fetch");
function fetchApi(obj=[], query='') {
    fetch(`https://api.bitflyer.com/v1/executions?product_code=FX_BTC_JPY&count=500${query}`).then(function (response) {
        return response.json();
    }).then(function(tmp) {
        json = obj.concat(tmp);
        console.log(json.length);
        if (json.length < 3000) {
            setTimeout(function(){ fetchApi(json, `&before=${tmp[tmp.length-1]['id']}`) }, 500);
        } else {
            const ranges = createRanges(json, 60*1000*0.1)
            const params = createParams(ranges)
            googleCharts(params);
        }
    });
}
fetchApi();
パッケージ追加方法

Anaconda promptで、-Dオプションを付与してnpm installを実行すればよい。[参考]

 npm i -D node-fetch
データ加工

取引データをレンジ(=一定の時間=span)単位にまとめて、ローソク足チャート描画用の属性情報を生成する。

function createRanges(obj, span) {
    const offset = Math.floor(Date.parse(obj[obj.length-1].exec_date)/span);

    // 取引情報をレンジIDをキーに集約
    let ranges = [];
    obj.forEach(o => {
        o.timestamp = Date.parse(o.exec_date);
        var rangeId = Math.floor(o.timestamp/span) - offset;
        if (ranges[rangeId]) {
            ranges[rangeId]['executions'].push(o);
        } else {
            ranges[rangeId] = {'id':rangeId, 'date':dateFormat(o.exec_date,'hh:MM:ss'),'executions':[o]}
        }
    });

    // 各レンジに属性情報を付与(レンジ単位計算)
    ranges.forEach(r => {
        r.executions.sort(function(a, b) {
          if (a.timestamp > b.timestamp) {
            return 1;
          } else {
            return -1;
          }
        })
        var priceList = r.executions.map(x => x.price)
        r['open'] = priceList[1];
        r['close'] = priceList.slice(-1)[0];
        r['high'] = Math.max(...priceList);
        r['low'] = Math.min(...priceList);
        r['volume'] = r.executions.map(x => x.size).reduce((a,b) => a + b);
    })

    // 各レンジに属性情報を付与(レンジ跨ぎ計算)
    closeList = ranges.map(x => x.close);
    ranges.forEach((r, idx, arr) => {
        r['avg05'] = avg(closeList, idx, 5);
        r['avg25'] = avg(closeList, idx, 25);
        r['avg50'] = avg(closeList, idx, 50);
    })

    return ranges;
}
日付フォーマット
function dateFormat(dateStr, format) {
    const date = new Date(Date.parse(dateStr));
    format = format.replace(/yyyy/, date.getFullYear());
    format = format.replace(/mm/, ('0'+date.getMonth() + 1).slice(-2));
    format = format.replace(/dd/, ('0'+date.getDate()).slice(-2));
    format = format.replace(/hh/, date.getHours());
    format = format.replace(/MM/, ('0'+date.getMinutes()).slice(-2));
    format = format.replace(/ss/, ('0'+date.getSeconds()).slice(-2));
    return format;
}
移動平均計算
function avg(arr, idx, num) { 
    if (idx - num >= 0) {
        const arrParts = arr.slice(idx-num+1, idx+1)
        return arrParts.reduce((a,b) => a+b, 0) / num;
    } else {
        return;
    } 
}
パラメータ生成

Google Charts用のパラメータ生成。dataoptionAPIリファレンスに準拠、chartTypecolumnsgoogleCharts関数の汎用性を高めるために用意したもの。

function createParams(ranges) {
    const commonOption = {
        colors: [ '#003A76' ],
        legend: { position: 'none' },
        bar: { groupWidth: '100%' },
        width: Math.max(ranges.length*10, 800),
        hAxis: { direction: 1 },
        vAxis: { viewWindowMode: 'maximized' }
    }

    const candleData = ranges.map(r => [r.date, r.low, r.open, r.close, r.high, r.avg05, r.avg25, r.avg50])
    const candleOption = { ...commonOption, ...{
        chartArea: { left: 80, top: 10, right: 80, bottom: 10 },
        height: 400,
        lineWidth: 2,
        curveType: 'function',
        seriesType: 'candlesticks',
        series:
            { '1': { type: 'line', color: 'green' },
              '2': { type: 'line', color: 'red' },
              '3': { type: 'line', color: 'orange' } } 
    }};

    const volumeData = ranges.map(r => [r.date, r.volume])
    const volumeOption = { ...commonOption, ...{
        chartArea: { left: 80, top: 10, right: 80, bottom: 80 },
        height: 200,
    }};

    return [
        {data:candleData, option:candleOption, chartType:'ComboChart', columns:['string'].concat(new Array(7).fill('number'))},
        {data:volumeData, option:volumeOption, chartType:'ColumnChart', columns:['string', 'number']}
    ];
}
グラフ描画

Google Chartsを使ってグラフを生成する。任意の数のグラフを描画できるようにするため、描画エリアとなるdiv要素も動的に生成するようにした。

function googleCharts(params) {
    const width = params[0].option.width + 30;
    const height = params.map(p => p.option.height).reduce((a,b) => a+b) + 30;
    params.map((p,i) => p.divId = `${p.chartType}${i}`);
    const divs = params.map((p,i) => `<div id='${p.divId}'></div>`).join('');
    const callers = params.map(p => `drawChart(${JSON.stringify(p).replace(/"/gi,'\'')});`).join('');
    const html = `<iframe width="${width}" height="${height}" srcdoc="
        <head><script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script></head>
        <body>
        ${divs}
        <script>
            var timeout;
            google.charts.load('current', {'packages': ['corechart'] });
            window.onload = function(){
               timeout = setInterval(function () {
                  if (google.visualization != undefined) {
                     ${callers}
                     clearInterval(timeout);
                  }
               }, 300);
            }
            function drawChart(param) {
                var chart = new google.visualization[param.chartType](document.getElementById(param.divId));
                var chartData = new google.visualization.DataTable();
                param.columns.forEach(c => chartData.addColumn(c));
                chartData.addRows(param.data);
                chart.draw(chartData, param.option);
            } 
        </script>
        </body>
    ">`;
    $$.html(html);
}
グラフ描画(モジュール化)

exportsオブジェクトの関数として定義することで、ノートブックを越えて使えるモジュールにすることもできる。

GoogleChartDrawer.js

exports.html = function (params) {
    var width = params[0].option.width + 30
    var height = params.map(p => p.option.height).reduce((a,b) => a+b) + 30
    params.map((p,i) => p.divId = `${p.chartType}${i}`)
    var divs = params.map((p,i) => `<div id='${p.divId}'></div>`).join('')
    var callers = params.map(p => `drawChart(${JSON.stringify(p).replace(/"/gi,'\'')});`).join('')
    return `<iframe width="${width}" height="${height}" srcdoc="
        <head><script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script></head>
        <body>
        ${divs}
        <script>
            var timeout;
            google.charts.load('current', {'packages': ['corechart'] });
            window.onload = function(){
               timeout = setInterval(function () {
                  if (google.visualization != undefined) {
                     ${callers}
                     clearInterval(timeout);
                  }
               }, 300);
            }
            function drawChart(param) {
                var chart = new google.visualization[param.chartType](document.getElementById(param.divId));
                var chartData = new google.visualization.DataTable();
                param.columns.forEach(c => chartData.addColumn(c));
                chartData.addRows(param.data);
                chart.draw(chartData, param.option);
            } 
        </script>
        </body>
    ">`;
}
exports.draw = function (params) {
    if ($$) {
        $$.html(exports.html(params))
    }
}

呼び出し方

var google = require('./GoogleChartDrawer')
google.draw(params)
タイトルとURLをコピーしました