Mini Tokyo 3D バージョン 2.3.0 リリース

Mini Tokyo 3D バージョン 2.3.0 がリリースされました (GitHub)。2.2.0 からの追加・修正機能を見ていきましょう。

JR八高線の八王子〜小川町間、川越線の川越〜高麗川間が開通

八王子と高崎を結ぶJR八高線の、八王子から東武東上線と接続する小川町までが開通しました。同時に、大宮〜川越間が開通していたJR川越線高麗川まで延伸して全線が開通しました。

八高線高麗川川越線に接続しており、八王子〜高麗川〜川越は電化しているため、この区間は直通運転が行われています。一方、八高線高麗川以北は非電化区間のため、運行は独立しています。両線とも単線なので、途中の駅で列車交換(対向列車の待ち合わせ)を行う、ピタゴラスイッチ的な動きの様子が見られます。

駅選択時のハイライト表示

駅にマウスポインタを合わせた時に、ポップアップ表示と共に駅の輪郭をハイライトして注目している場所がわかりやすくなりました。

駅出口情報の表示

駅をクリックまたはタップして選択すると、ズームインして駅出口の場所を表示するようになりました。ぞれぞれ地図上に出口番号が出るほか、出口の一覧も表示されます。一覧の中で出口の名前にマウスポインタを合わせたり、タップをすると地図上の出口の名前がハイライトされ、場所の確認をすることができます。

地上モードに切り替えるとビルや歩道との対応もわかりやすいです。まずは銀座線の全駅に対応。順次、地下鉄の駅をカバーする予定です。

マップスタイルの更新による道路・地下街の詳細表示

Mapboxのマップスタイルを更新して、ズームレベル15以上で道路と地下街の詳細なポリゴンデータが表示されるようになりました。この機能は、日本独自のゼンリンの地図データがベースになっているもので、しばらく前からデータは提供されていたのですが、ようやく Mini Tokyo 3D でも対応ができました。

右がこれまでのマップスタイルで、左が新しくなったマップスタイルです。道路や歩道が正確な幅で描かれ、中央分離帯の緑や日比谷公園の遊歩道も見分けられるようになっているのがおわかりいただけると思います。

f:id:nagixx:20200815204445p:plain

地下表示モードでは、下のように大手町〜東京〜銀座の地下街が大きく広がりを持っているのがわかります。

f:id:nagixx:20200815204547p:plain

花火レイヤーの追加

3D マップ上で花火大会を再現しました。7/23〜26の4連休と8/8〜10の3連休で、隅田川花火大会、足立の花火、幕張ビーチ花火フェスタ、みなとみらいスマートフェスティバル、神宮外苑花火大会江戸川花火大会、TOKYOいたばし花火フェスティバル/戸田橋花火大会をバーチャル開催。再生モードで時間をさかのぼれば、花火の打ち上げを見ることができますよ。

JR特急列車の追加

特急サフィール踊り子E261系、特急踊り子E259系2000番台、寝台特急サンライズ出雲サンライズ瀬戸285系、特急ひたち・ときわE657系、特急スワローあかぎ・あかぎ・草津651系に対応。

イベントリスナAPIの追加

バージョン 2.3.0 では MiniTokyo3D クラスにイベントリスナの追加・削除を行う次のメソッドが追加されました。

メソッド 説明
off イベントリスナを削除
on イベントリスナを追加
once 一度だけ呼び出されるリスナを追加

上記のメソッドで指定するイベントリスナを使用して、マップで発生した次のイベントを受け取ることができます。

イベント 説明
boxzoomcancel 「ボックスズーム」操作がキャンセル
boxzoomend 「ボックスズーム」操作が終了
boxzoomstart 「ボックスズーム」操作が開始
click ポインティングデバイを押して離す
contextmenu マウスの右ボタンがクリック
dblclick ポインティングデバイスを2回連続して押して離す
drag 「移動のためのドラッグ」操作中
dragend 「移動のためのドラッグ」操作が終了
dragstart 「移動のためのドラッグ」操作が開始
error エラーが発生
load 必要なリソースのダウンロード・表示が完了
mousedown ポインティングデバイスが押される
mousemove ポインティングデバイスが移動
mouseover カーソルがマップまたは子要素に入る
mouseup ポインティングデバイスが離された
move あるビューから別のビューへのアニメーション遷移中
moveend あるビューから別のビューへの遷移を完了
movestart あるビューから別のビューに遷移する直前
pitch 傾きの状態遷移アニメーションの間
pitchend 傾きが変化し終わった直後
pitchstart 傾きが変化し始める直前
resize マップのサイズが変更
rotate 「回転のためのドラッグ」操作中
rotateend 「回転のためのドラッグ」操作が終了
rotatestart 「回転のためのドラッグ」操作が開始
touchcancel touchcancel イベントが発生
touchend touchend イベントが発生
touchmove touchmove イベントが発生
touchstart touchstart イベントが発生
wheel wheel イベントが発生
zoom あるズームレベルから別のズームレベルへのアニメーション遷移中
zoomend あるズームレベルから別のズームレベルへの移行を完了した直後
zoomstart あるズームレベルから別のズームレベルへの移行を開始する直前

詳細は Mini Tokyo 3D 開発者ガイドをご覧ください。

Mini Tokyo 3D バージョン 2.2.0 リリース

Mini Tokyo 3D バージョン 2.2.0 がリリースされました (GitHub)。2.1.0 からの追加・修正機能を見ていきましょう。

東武伊勢崎線東武動物公園〜久喜間、千葉モノレールが全線開通

東武伊勢崎線は、浅草から東武動物公園までの「東武スカイツリーライン」と路線愛称が付いている区間は開通していたものの、JR 宇都宮線との接続駅である久喜までは達していませんでした。今回、2駅分ですが Mini Tokyo 3D マップの北の端である久喜まで開通し、特急りょうもうなどの伊勢崎方面の列車が走り始めました。

また、千葉モノレール 1号線の千葉みなと〜県庁前間、2号線の千葉〜千城台間が全線開通しました。千葉モノレールは長らく稲毛海岸方面や青葉病院への延伸計画があったのですが、こちらは採算性の懸念から昨年に延伸中止が決定されたみたいですね。

東武東上線・小川町、小田急小田原線伊勢原まで延伸

東武東上線はこれまで川越市止まりだった路線が、JR 八高線との接続駅である小川町まで延伸しました(八高線はまだ未通ですが)。

小田急小田原線は、本厚木止まりだった路線が伊勢原まで延伸しました。

駅詳細情報のポップアップ

駅にマウスポインタを合わせる(タッチデバイスの場合はタッチする)と駅のサムネイル画像と路線名がポップアップで表示されるようになりました。現在、乗り換え駅の重複を含めて 2,087 駅あるのですが、すべての駅に対応しています。初めて行く場所でもなんとなく街の雰囲気がわかりますね。

列車・フライトのシェア

追跡中の列車・フライトのシェア機能を追加しました。「この列車をシェア」ボタンを押して SNS やメール、AirDrop 等を選ぶと相手 に URL が送信され、開くと同じ列車を追跡した状態で見られます。相手側でアプリをインストールすることなく、ブラウザだけで見ることができるので便利です。待ち合わせや、帰宅時の連絡などにお使いください。

韓国語翻訳の改善

一部の韓国語の駅名表記を、鉄道事業者が使用している公式の表記に差し替えました。

ナビゲーションに関する API の追加

バージョン 2.1.0 で追加された開発者向け API に関してですが、MiniTokyo3D クラスにマップの操作を行う次のメソッドが追加されました。

メソッド 説明
getCenter マップの中心座標を取得
setCenter マップの中心座標を設定
getZoom マップのズームレベルを取得
setZoom マップのズームレベルを設定
getBearing マップの方角を取得
setBearing マップの方角を設定
getPitch マップの傾きを取得
setPitch マップの傾きを設定
easeTo マップをアニメーション付きで移動
flyTo マップを飛行をイメージしたアニメーション付きで移動
jumpTo マップをアニメーションなしで移動

詳細は Mini Tokyo 3D 開発者ガイドをご覧ください。

追跡中のズームレベル・傾きの変更

これまで列車や旅客機を追跡中は、画面上のボタンやキーボードを使ったズームレベルと傾きの変更はできませんでしたが、変更可能になりました。

マウスオーバー時の反応改善

列車や旅客機とマウスオーバーのポップアップが重なっていると、マウスを別の列車や旅客機上に動かしても表示が切り替わらない場合がありましたが、すぐに切り替わるように動作を改善しました。

フルスクリーンモードの不具合修正

フルスクリーンモードで駅名検索のテキストフィールドや追跡時の列車時刻表が表示されない不具合を修正しました。

Edge ブラウザにおける駅名検索の不具合修正

Edge ブラウザで、駅名の自動補完リストから駅を選択しても移動しない不具合を修正しました。

Mini Tokyo 3D 開発者向け機能の提供をスタート

f:id:nagixx:20200612154559j:plain

ほぼ 1 年前から開発を開始した、東京の公共交通のリアルタイム3Dマップ Mini Tokyo 3D のバージョン 2.1 をリリースしました。

おかげさまで Mini Tokyo 3D は第3回東京公共交通オープンデータチャレンジ最優秀賞を受賞したほか、2019年度 VLED 勝手表彰貢献賞GitHub スター数 1,300 超えなど、各方面から熱いご支持をいただいています。

Mini Tokyo 3D の内容や開発の経緯については、すでに記事がいくつか出ているほか、日々の開発状況のツイートをまとめた Togetter まとめをご覧いただくとして、本記事では新バージョンで導入したクリエーター・開発者向けの機能についてご紹介します。

Mini Tokyo 3D のマップを Web ページに埋め込む

Mini Tokyo 3D のマップを Web ページに埋め込むのはとても簡単。Mini Tokyo 3D は地図タイルに Mapbox のサービスを利用しているため、まず Mapbox のアクセストークンを入手しましょう。Mini Tokyo 3D 開発者ガイドの「Mapbox アクセストークンの入手」の手順に従って、アクセストークン文字列を取得します。

そして埋め込みを行いたい Web ページのヘッダに、スタイルシートJavaScript ファイルのリンクを追加。 

<head>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/mini-tokyo-3d@latest/dist/mini-tokyo-3d.min.css" />
  <script src="https://cdn.jsdelivr.net/npm/mini-tokyo-3d@latest/dist/mini-tokyo-3d.min.js"></script>
</head>

ページ本文には、マップを表示する <div> エレメントを配置して、<script> エレメントに MiniTokyo3D オブジェクトを作成する JavaScript コードを記述します。オブジェクトに渡すオプションには、 <div> エレメントの ID と、先ほど入手した Mapbox アクセストークンを指定するのを忘れずに。

<body>
  <div id="mini-tokyo-3d" style="width: 400px; height: 400px;"></div>

  <script>
    const options = {
      container: 'mini-tokyo-3d',
      secrets: {
        mapbox: '<Mapbox アクセストークン>'
      }
    };
    const mt3d = new MiniTokyo3D(options);
  </script>
</body>

はい、これだけです。 <div> エレメントのスタイルを編集して好きなサイズにしたり、ページいっぱいに表示したりしてくださいね。

Mini Tokyo 3D API でカスタマイズ

Mini Tokyo 3D が提供する API を利用すれば、マップの表示内容や動作を柔軟にカスタマイズすることができます。上記の例のコードに、次のオプションを追加してみましょう。

  <script>
    const options = {
      container: 'mini-tokyo-3d',
      secrets: {
        mapbox: '<Mapbox アクセストークン>'
      },
      center: [139.767, 35.6806],
      zoom: 16
      bearing: 90,
      pitch: 15
    };
    const mt3d = new MiniTokyo3D(options);
  </script>

centerzoombearingpitchはそれぞれマップの初期座標(経度・緯度)、ズームレベル、方角(北を 0 として反時計回りの角度)、傾き(真上を 0 とした角度)を表しています。デフォルトだと東京駅周辺を南方向から見たマップになりますが、この設定では新宿駅周辺を東方向やや上方から眺めた感じになります。

利用可能なオプションの詳細は、開発者ガイドの「Mini Tokyo 3D API」を参照してください。

バージョン 2.1 は初めての開発者向け機能のリリースということで、まだオブジェクト作成時の初期設定オプションしか変えることができませんが、今後のリリースではマップ内オブジェクトの操作やイベントコールバック、プラグイン API を介したカスタムレイヤーの追加など、よりインタラクティブなコントロールを可能にすることを構想しています。

モジュールとしてアプリに組み込む

今回のリリースのタイミングで npm パッケージとしての公開を始めています。皆さんのアプリの部品としてお使いいただけます。

まず、Mini Tokyo 3D の npm モジュールをインストールし、皆さんのアプリケーションの package.json に登録します。

npm install mini-tokyo-3d --save

CommomJS 形式でモジュールを読み込む場合は、コードの先頭で次のように記載します。

const MiniTokyo3D = require('mini-tokyo-3d');

ES6 形式でモジュールを読み込む場合は、コードの先頭で次のように記載します。

import MiniTokyo3D from 'mini-tokyo-3d';

アプリケーションのコード内で、次のようにして MiniTokyo3D オブジェクトを初期化します。options オブジェクトの container には Mini Tokyo 3D がマップを表示する HTML エレメントの ID を指定します。また、secrets.mapbox には、上のステップで入手した Mapbox アクセストークンを指定します。

const options = {
  container: '<コンテナエレメントの ID>',
  secrets: {
    mapbox: '<mapbox アクセストークン>'
  }
};
const mt3d = new MiniTokyo3D(options);

こちらも詳細は、開発者ガイドの「モジュールとしてアプリに組み込む」を参照してください。

スポンサー募集中!

最後になりますが、GitHub スポンサーの募集も始めました。皆さんのご支援は今後の開発の大きなモチベーションになりますので、Mini Tokyo 3D を気に入っていただけましたらぜひよろしくお願いします!

github.com

Chart.js のチャートを格段に綺麗に見せるカラーパレットプラグイン

f:id:nagixx:20181126195706p:plain

動的なチャートを作って Web ページに貼り付けるのに Chart.js はとても便利ですが、難点は1つずつ色の指定をしなければいけないところです。例えばこんな感じで棒グラフを作るとして、Dataset ごとに背景色を指定する必要があります(色を指定しないとグレー一色になってしまう)。

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.js"></script>
<canvas id="myChart"></canvas>
<script type="text/javascript">
var ctx = document.getElementById('myChart').getContext('2d');
var myChart = new Chart(ctx, {
  type: 'bar',
  data: {
    labels: ['2015年', '2016年', '2017年', '2018年'],
    datasets: [{
      label: 'Dataset 1',
      data: [4, 15, 12, 23],
      backgroundColor: 'rgb(255, 0, 0)'
    }, {
      label: 'Dataset 2',
      data: [10, 9, 3, 16],
      backgroundColor: 'rgb(0, 255, 0)'
    }, {
      label: 'Dataset 3',
      data: [12, 20, 14, 3],
      backgroundColor: 'rgb(0, 0, 255)'
    }]
  }
});
</script>

ところがここで面倒臭がって赤、緑、青でいいや、などとしてしまうと、このような絶望的な見栄えのチャートになってしまいます。

chart1

気の利いたカラーパレットをデフォルトで適用してくれれば良さそうなものの、実際のところそうはなっておらず、ページ作成者のデザインセンスがモロに出てしまうのは深刻な問題です。

そういうわけで、ここはひとつプラグインでも作るか、ということでできたのが chartjs-plugin-colorschemes です。使い方は下記の順番で .js ファイルを <head>〜</head> で読み込んでおくだけ。先ほどの backgroundColor のプロパティは削除しておきます。

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js"></script>
<script type="text/javascript" src="https://github.com/nagix/chartjs-plugin-colorschemes/releases/download/v0.2.0/chartjs-plugin-colorschemes.min.js"></script>

chart2

なんということでしょう。あんなにみすぼらしかったチャートが(以下略)。

現時点では、カラーパレットはデータビジュアライゼーション界隈でポピュラーな ColorBrewerTableau から拝借しております。ColorBrewer はペンシルベニア州立大学の地理学者 Cynthia A. Brewer が考案したカラーパレットで、人間の視覚を考慮して色の区分が明瞭になるように工夫されており、連続的 (Sequencial)、発散的(diversing)、定性的(Qualitative)の3つのカテゴリそれぞれに複数の色のセットが用意されています。Tableau はシアトルに本社を置くタブローソフトウェアが開発しているデータビジュアライゼーションの商用製品で、こちらもチャートでの利用に最適なカラーパレットを数多く備えています。

デフォルトでは次の brewer.Paired12 カラーパレットが自動的に適用されますが、自分で指定するには次のように 一覧から選んだカラースキーマをオプションに記述します。

brewer.Paired12:                        
var myChart = new Chart(ctx, {
  // ...
  options: {
    plugins: {
      colorschemes: {
        scheme: 'brewer.Paired12'
      }
    }
  }
});

下記にいくつかのサンプルを載せておきます。インタラクティブに動作するチュートリアルページでも色を確認しながら、ぜひ色々なカラーパレットをお試しください。

chart3

chart4

chart5

chart6

拡大縮小・スクロールを無効にする方法がiOS 11.3 Safariで効かなくなった問題と解決法

その昔、スマホ向けWebページの拡大縮小・スクロールをさせたくない場合にはviewportuser-scalable=noを指定するという手軽な方法が利用可能でしたが、2016年のiOS 10のリリース以降はこの方法が使えなくなったため、JavaScriptを使ってtouchstartイベントやtouchmoveイベントのハンドラでpreventDefault()を呼ぶ方法がポピュラーになっていると思います。

が、つい最近iOS 11.3 Safariでこの方法が効かなくなっていることに気づきました。この変更は結構多くのライブラリの動作に影響を与えており、一部の開発者の間で騒ぎになっていてWebKitのバグにも登録されました。しかし、WebKitの開発者ではこれはバグではなく想定された仕様変更だということで、ページ製作者の側が対応する必要があります。

Bug 182521 – REGRESSION (iOS 11.3 beta): touchmove preventDefault() no longer respected

具体的にどんな変更があったかというと、bodydocumentwindowtouchstartおよびtouchmoveイベントハンドラのデフォルト動作が、passiveになったとのこと。passiveイベントハンドラではpreventDefault()の呼び出しが無視されてしまうわけです。イベントハンドラの動作オプションについては下記の記事を参考にしてください。

そもそもなんのための変更かというと、これによりイベントハンドラ内で拡大縮小・スクロール動作が抑止されないことがあらかじめわかるため、スクロール動作のレスポンスがかなり改善されるのです。

では、どうすれば以前のような動作にできるかというと、MDNにヒントがあります。一番下の方の「passive なリスナーを用いたスクロールのパフォーマンスの改善」というところです。よく見るとChromeも55からtouchstartおよびtouchmoveのデフォルト動作がpassiveになってますね。

ちょっとコードを抜粋して手を加えると、このような感じです。現在、ほぼすべてのブラウザがaddEventListener()の第3引数にpassiveプロパティを含むオブジェクトを受け付けるようにはなっているのですが、互換性を重視するなら下記のような検出コードを使うとよいと思います。

/* "passive" が使えるかどうかを検出 */
var passiveSupported = false;
try {
    document.addEventListener("test", null, Object.defineProperty({}, "passive", {
        get: function() {
            passiveSupported = true;
        }
    }));
} catch(err) {}

/* リスナーを登録 */
document.addEventListener('touchstart', function listener(e) {
    /* do something */
e.preventDefault(); }, passiveSupported ? { passive: false } : false);
document.addEventListener('touchmove', function listener(e) { /* do something */
e.preventDefault(); }, passiveSupported ? { passive: false } : false);

最後におまけ。こちらのページにも上記の修正を入れておきました。

Drawing a real-time Bitcoin chart using Chart.js

(*日本語の記事はこちら)

Chart.js is a popular JavaScript chart library that enables to create dynamic, beautiful charts easily. I recently made chartjs-plugin-streaming, a Chart.js plugin for live streaming data with the auto-scroll feature. It is suitable for IoT-related use cases such as sensor data monitoring, and when I was looking for some real examples of streaming data, I noticed that it is also good for displaying real-time digital currency trading data that the digital currency exchanges provide. So, I just tried that out.

It is becoming more common for the exchanges to deliver trading data efficiently using WebSocket. Some of them require authentication, but in this article, let's create trading charts for the exchanges that provide public API without authentication.

First example: Bitfinex WebSocket API

First, we need to include the following required libraries.

Put the following tags between <head> and </head>.

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.6.0/Chart.js"></script>
<script type="text/javascript" src="https://github.com/nagix/chartjs-plugin-streaming/releases/download/v1.1.0/chartjs-plugin-streaming.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pusher/4.1.0/pusher.js"></script>

Next, let's add a canvas to the page. Include an id so that you can identify the chart later.

<canvas id="Bitfinex"></canvas>

Then, prepare an object for buffering data, which is buf. Set the array that has two empty array elements to the property with the same name as canvas's id. The first array is for 'buy' prices and the second is for 'sell' prices.

<script type="text/javascript">
var buf = {};
buf['Bitfinex'] = [[], []];
</script>

Now, let's access the real data. We are going to collect real-time trading data using WebSocket from Bitfinex in Hong Kong, which is one of the largest exchanges by BTC/USD trading volume. You can subscribe trading data of the Bitcoin/US dollar pair by sending a request like below to the URI wss://api.bitfinex.com/ws provided by Bitfinex. (See Bitfinex WebSocket API for details)

{
    "event": "subscribe", // subscribe request
    "channel": "trades",  // trade data
    "pair": "BTCUSD"      // Bitcoin/US dollar
}

The data you will receive through the callback function is like this.

[
    5,             // 0: channel ID
    'te',          // 1: message type
    '1234-BTCUSD', // 2: sequence ID
    1443659698,    // 3: timestamp
    236.42,        // 4: price
    0.49064538     // 5: amount bought (positive) or sold (negative)
]

As we only need the trade type, the timestamp (X-axis) and the price (Y-axis), the filtered data is stored into the buffer on every message receipt using the following code.

var ws = new WebSocket('wss://api.bitfinex.com/ws/');
ws.onopen = function() {
    ws.send(JSON.stringify({      // send subscribe request
        "event": "subscribe",
        "channel": "trades",
        "pair": "BTCUSD"
    }));
};
ws.onmessage = function(msg) {     // callback on message receipt
    var response = JSON.parse(msg.data);
    if (response[1] === 'te') {    // Only 'te' message type is needed
        buf['Bitfinex'][response[5] > 0 ? 0 : 1].push({
            x: response[3] * 1000, // timestamp in milliseconds
            y: response[4]         // price in US dollar
        });
    }
}

Finally, we need to configure the chart. Regarding the details of the chart customization, please refer to the Chart.js official documentation. The key points here are that the 'realtime' scale that the chartjs-plugin-streaming plugin provides are set to the X axis, and that the entire data stored in the buffer is added to the chart in the onRefresh callback function that is called at a regular interval (every second by default).

var id = 'Bitfinex';
var ctx = document.getElementById(id).getContext('2d');
var chart = new Chart(ctx, {
    type: 'line',
    data: {
        datasets: [{
            data: [],
            label: 'Buy',                     // 'buy' price data
            borderColor: 'rgb(255, 99, 132)', // line color
            backgroundColor: 'rgba(255, 99, 132, 0.5)', // fill color
            fill: false,                      // no fill
            lineTension: 0                    // straight line
        }, {
            data: [],
            label: 'Sell',                    // 'sell' price data
            borderColor: 'rgb(54, 162, 235)', // line color
            backgroundColor: 'rgba(54, 162, 235, 0.5)', // fill color
            fill: false,                      // no fill
            lineTension: 0                    // straight line
        }]
    },
    options: {
        title: {
            text: 'BTC/USD (' + id + ')', // chart title
            display: true
        },
        scales: {
            xAxes: [{
                type: 'realtime' // auto-scroll on X axis
            }]
        },
        plugins: {
            streaming: {
                duration: 300000, // display data for the latest 300000ms (5 mins)
                onRefresh: function(chart) { // callback on chart update interval
                    Array.prototype.push.apply(
                        chart.data.datasets[0].data, buf[id][0]
                    );            // add 'buy' price data to chart
                    Array.prototype.push.apply(
                        chart.data.datasets[1].data, buf[id][1]
                    );            // add 'sell' price data to chart
                    buf[id] = [[], []]; // clear buffer
                }
            }
        }
    }
});

Below is the completed chart. You can see that the chart is scrolling from the right to the left slowly.

Bitfinex

Bitstamp WebSocket API

The next example is Bitstamp, an exchange in UK, which is the central trading hub in Europe. Bitstamp uses Pusher, a Pub/Sub messaging library for real time WebSocket streaming. Therefore, the data subscription code is much simpler. (See Bitstamp WebSocket API for details)

The data you will receive through the callback function is like this.

{
    id: 17044523,            // trade unique ID
    amount: 1,               // amount
    price: 2496.21,          // price
    type: 1,                 // trade type (0: buy, 1: sell)
    timestamp: "1499472674", // timestamp
    buy_order_id: 47485421,  //	buy order ID
    sell_order_id: 47485426  //	sell order ID
}

Below is the code that receives data. It also takes only the trade type, the timestamp and the price.

buf['Bitstamp'] = [[], []]; // prepare buffer
var pusher = new Pusher('de504dc5763aeef9ff52'); // Pusher key for Bitstamp
var channel = pusher.subscribe('live_trades'); // subscribe live trade data
channel.bind('trade', function (data) { // callback on message receipt
    buf['Bitstamp'][data.type].push({
        x: data.timestamp * 1000, // timestamp in milliseconds
        y: data.price             // price in US dollar
    });
});

The configuration of the chart is the same as above except for the id. Below is the completed chart.

Bitstamp

BTC-E WebSocket API

BTC-E is also a popular exchage located in Bulgaria. It uses Pusher for streaming as well. (See BTC-E WebSocket API for details)

Below is the data you will receive.

[
    [
        "buy",       // 0: trade type
        "2476.999",  // 1: price
        "0.08863539" // 2: amount
    ]
]

As timestamps are not included in case of BTC-E, set the current time using Date.now().

buf['BTC-E'] = [[], []]; // prepare buffer
var pusher = new Pusher('c354d4d129ee0faa5c92'); // Pusher key for BTC-E
var channel = pusher.subscribe('btc_usd.trades'); // subscribe BTC/USD trades
channel.bind('trades', function (dataset) { // callback on message receipt
    dataset.forEach(function(data) {
        buf['BTC-E'][data[0] === 'buy' ? 0 : 1].push({
            x: Date.now(), // timestamp in milliseconds
            y: data[1]     // price in US dollar
        });
    });
});

BTC-E

BitMEX WebSocket API

BitMEX is an exchange in Hong Kong, which is famous for high leverage trading. It provides a normal WebSocket API. Below are the request and the message to be received. (See BitMEX WebSocket API for details)

{
    "op": "subscribe", // subscribe request
    "args": [
        "trade:XBTUSD" // Bitcoin/US dollar
    ]
}
{
    table: "trade",
    action: "insert",
    data: [
        {
            timestamp: "2017-07-09T01:39:30.866Z", // timestamp
            symbol: "XBTUSD",         // currency pair symbol
            side: "Buy",              // trade type
            size: 34,                 // amount
            price: 2548.9,            // price
            tickDirection: "ZeroPlusTick", // tick direction
            trdMatchID: "34d6de97-5d54-3431-e505-ffc3bc8c58ef",
            grossValue: 2039284,      // gross value 
            homeNotional: 0.02039284, // notional in Bitcoin
            foreignNotional: 52       // notional in US dollar
        }
    ]
}

The code and chart are as follows.

buf['BitMEX'] = [[], []]; // prepare buffer
var ws = new WebSocket('wss://www.bitmex.com/realtime');
ws.onopen = function() {
    ws.send(JSON.stringify(    // send subscribe request
       "op": "subscribe",
       "args": [
            "trade:XBTUSD"
        ]
    }));
};
ws.onmessage = function(msg) { // callback on message receipt
    var response = JSON.parse(msg.data);
    response.data.forEach(function(data) {
        buf['BitMEX'][data.side === 'Buy' ? 0 : 1].push({
            x: data.timestamp, // timestamp
            y: data.price      // price in US dollar
        });
    });
}

BitMEX

CoinCheck WebSocket API

The last one is Japanese exchange, CoinCheck. Only BTC/Japanese-yen rates are delivered here, so let's display them. Below are the request and the message to be received. (See CoinCheck WebSocket API for details)

{
    "type": "subscribe",        // subscribe request
    "channel": "btc_jpy-trades" // Bitcoin/Japanese yen
}
[
    9856377,    // trade ID
    "btc_jpy",  // currency pair
    "289544.0", // price
    "0.0367",   // amount
    "sell"      // trade type
]

The code and chart are as follows.

buf['CoinCheck'] = [[], []];
var ws = new WebSocket('wss://ws-api.coincheck.com/');
ws.onopen = function() {
    ws.send(JSON.stringify({        // send subscribe request
        "type": "subscribe",
        "channel": "btc_jpy-trades"
    }));
};
ws.onmessage = function(msg) { // callback on message receipt
    var response = JSON.parse(msg.data);
    buf['CoinCheck'][response[4] === 'buy' ? 0 : 1].push({
        x: Date.now(), // timestamp in milliseconds
        y: response[2] // price in Japanese yen
    });
}

CoinCheck WebSocket API

There are many other digital currency exchanges that provide API to deliver real-time trading data. The API and data format are slightly different each other, but you can draw a chart with minimum modification. I hope this article helps you try out for yourself.

Bitcoin のリアルタイムチャートを Chart.js で表示する

(*English translation is here)

Chart.js は動的で美しいチャートを手軽に作ることができるポピュラーな JavaScript ライブラリです。先日、リアルタイムストリーミングデータの表示に便利な自動スクロール機能を実装した chartjs-plugin-streaming プラグインを作りました。センサーデータのモニタリング等の IoT 関連の用途に最適ですが、何かリアルなストリーミングデータないかなー、と探していたところ、仮想通貨の取引所が配信している取引価格の表示にもぴったりな感じなので、早速試してみました。

最近は WebSocket を使って効率的にストリーミング配信を行う取引所も増えてきており、中には認証を必要とする場合もあるのですが、本記事では特に認証の不要なパブリック API を提供している取引所の取引チャートを作ってみましょう。

最初の例: Bitfinex WebSocket API

まずは必要なライブラリを組み込みます。

下記を<head>〜</head>にでも書いておきます。

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.6.0/Chart.js"></script>
<script type="text/javascript" src="https://github.com/nagix/chartjs-plugin-streaming/releases/download/v1.2.0/chartjs-plugin-streaming.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pusher/4.1.0/pusher.js"></script>

次に、ページに Canvas を設置します。チャートを区別できるように、id をつけておきます。

<canvas id="Bitfinex"></canvas>

そして、データをバッファリングしておくためのオブジェクト buf を用意します。先ほど設置した Canvas の id と同じ名前のプロパティに、2つの空の配列要素を持つ配列を用意しておきます。1つ目の要素は買い値用、2つ目は売り値用です。

<script type="text/javascript">
var buf = {};
buf['Bitfinex'] = [[], []];
</script>

いよいよ実際のデータを取りに行く番です。WebSocket を使って、対米ドル取引で最大手の取引所 Bitfinex(香港)のリアルタイム取引データを取得します。Bitfinex が用意している URI wss://api.bitfinex.com/ws に次のような内容のリクエストを送信することで、ビットコイン/米ドルのペアの取引データを購読(Subscribe)することができます。(Bitfinex WebSocket API の詳細はこちら

{
    "event": "subscribe", // 購読リクエスト
    "channel": "trades",  // 取引データ
    "pair": "BTCUSD"      // ビットコイン/米ドル
}

コールバック関数を介して取得できるデータは次のような形式です。

[
    5,             // 0: チャンネル ID
    'te',          // 1: メッセージタイプ
    '1234-BTCUSD', // 2: シーケンス ID
    1443659698,    // 3: タイムスタンプ
    236.42,        // 4: 価格
    0.49064538     // 5: 取引量(買いならプラス、売りならマイナス)
]

必要なデータは売り買いの種別、タイムスタンプ(X軸)、価格(Y軸)だけなので、次のようなコードで更新があるたびにバッファにデータを追加していきます。

var ws = new WebSocket('wss://api.bitfinex.com/ws/');
ws.onopen = function() {
    ws.send(JSON.stringify({      // 購読リクエストを送信
        "event": "subscribe",
        "channel": "trades",
        "pair": "BTCUSD"
    }));
};
ws.onmessage = function(msg) {     // メッセージ更新時のコールバック
    var response = JSON.parse(msg.data);
    if (response[1] === 'te') {    // メッセージタイプ 'te' だけを見る
        buf['Bitfinex'][response[5] > 0 ? 0 : 1].push({
            x: response[3] * 1000, // タイムスタンプ(ミリ秒)
            y: response[4]         // 価格(米ドル)
        });
    }
}

最後にチャートの設定です。Chart.js のチャートのカスタマイズについては公式ドキュメントを参考にしてもらうとして、ここでのポイントは chartjs-plugin-streaming プラグインにより追加される 'realtime' タイプの目盛りを X 軸に設定していることと、定期的に(デフォルトでは1秒に1回)呼び出される onRefresh コールバック関数の中で、バッファに溜まっているデータを丸々チャートに追加しているところです。

var id = 'Bitfinex';
var ctx = document.getElementById(id).getContext('2d');
var chart = new Chart(ctx, {
    type: 'line',
    data: {
        datasets: [{
            data: [],
            label: 'Buy',                     // 買い取引データ
            borderColor: 'rgb(255, 99, 132)', // 線の色
            backgroundColor: 'rgba(255, 99, 132, 0.5)', // 塗りの色
            fill: false,                      // 塗りつぶさない
            lineTension: 0                    // 直線
        }, {
            data: [],
            label: 'Sell',                    // 売り取引データ
            borderColor: 'rgb(54, 162, 235)', // 線の色
            backgroundColor: 'rgba(54, 162, 235, 0.5)', // 塗りの色
            fill: false,                      // 塗りつぶさない
            lineTension: 0                    // 直線
        }]
    },
    options: {
        title: {
            text: 'BTC/USD (' + id + ')', // チャートタイトル
            display: true
        },
        scales: {
            xAxes: [{
                type: 'realtime' // X軸に沿ってスクロール
            }]
        },
        plugins: {
            streaming: {
                duration: 300000, // 300000ミリ秒(5分)のデータを表示
                onRefresh: function(chart) { // データ更新用コールバック
                    Array.prototype.push.apply(
                        chart.data.datasets[0].data, buf[id][0]
                    );            // 買い取引データをチャートに追加
                    Array.prototype.push.apply(
                        chart.data.datasets[1].data, buf[id][1]
                    );            // 売り取引データをチャートに追加
                    buf[id] = [[], []]; // バッファをクリア
                }
            }
        }
    }
});

出来上がったチャートはこのような感じ。ゆっくりスクロールしているのがわかるでしょうか。

Bitfinex

Bitstamp WebSocket API

次は欧州での取引の中心となっているイギリスの取引所 Bitstamp の例です。Bitstamp ではデータのストリーミングに Pub/Sub メッセージングライブラリ Pusher を利用しています。このため、データの取得コードはよりシンプルです。(Bitstamp WebSocket API の詳細はこちら

コールバック関数を介して取得できるデータは次のような形式です。

{
    id: 17044523,            // 取引固有 ID
    amount: 1,               // 取引量
    price: 2496.21,          // 価格
    type: 1,                 // 取引タイプ(0: 買い、1: 売り)
    timestamp: "1499472674", // タイムスタンプ
    buy_order_id: 47485421,  //	買い注文 ID
    sell_order_id: 47485426  //	売り注文 ID
}

データの取得コードは次の通りです。こちらも売り買いの種別、タイムスタンプ、価格のみを見ています。

buf['Bitstamp'] = [[], []]; // バッファを用意
var pusher = new Pusher('de504dc5763aeef9ff52'); // Bitstamp 専用のキーを指定
var channel = pusher.subscribe('live_trades'); // ライブ取引データを購読
channel.bind('trade', function (data) { // データ更新時のコールバック
    buf['Bitstamp'][data.type].push({
        x: data.timestamp * 1000, // タイムスタンプ(ミリ秒)
        y: data.price             // 価格(米ドル)
    });
});

チャートの設定は id の部分を変えるだけで全く同じです。完成したチャートはこの通り。

Bitstamp

BTC-E WebSocket API

続いてこちらも有名なブルガリアの取引所 BTC-E。BTC-E でも Pusher を使った配信が行われています。(BTC-E WebSocket API の詳細はこちら

データの形式は次の通り。

[
    [
        "buy",       // 0: 取引タイプ
        "2476.999",  // 1: 価格
        "0.08863539" // 2: 取引量
    ]
]

BTC-E ではタイムスタンプが流れてこないので、Date.now() で現在の時刻をセットしています。

buf['BTC-E'] = [[], []]; // バッファを用意
var pusher = new Pusher('c354d4d129ee0faa5c92'); // BTC-E 専用のキー
var channel = pusher.subscribe('btc_usd.trades'); // 対米ドル取引データを購読
channel.bind('trades', function (dataset) { // データ更新時のコールバック
    dataset.forEach(function(data) {
        buf['BTC-E'][data[0] === 'buy' ? 0 : 1].push({
            x: Date.now(), // タイムスタンプ(ミリ秒)
            y: data[1]     // 価格(米ドル)
        });
    });
});

BTC-E

BitMEX WebSocket API

ハイレバレッジ取引で有名な香港の BitMEX です。ここは素の WebSocket API です。リクエストとデータの形式は次の通りです。(BitMEX WebSocket API の詳細はこちら

{
    "op": "subscribe", // 購読リクエスト
    "args": [
        "trade:XBTUSD" // ビットコイン/米ドル
    ]
}
{
    table: "trade",
    action: "insert",
    data: [
        {
            timestamp: "2017-07-09T01:39:30.866Z", // タイムスタンプ
            symbol: "XBTUSD",   // 通貨ペアシンボル
            side: "Buy",        // 取引タイプ
            size: 34,           // 取引量
            price: 2548.9,      // 価格
            tickDirection: "ZeroPlusTick", // Tick の方向
            trdMatchID: "34d6de97-5d54-3431-e505-ffc3bc8c58ef",
            grossValue: 2039284,      // 総価値
            homeNotional: 0.02039284, // 想定元本(ビットコイン)
            foreignNotional: 52       // 想定元本(米ドル)
        }
    ]
}

コードとチャートはこのようになります。

buf['BitMEX'] = [[], []]; // バッファを用意
var ws = new WebSocket('wss://www.bitmex.com/realtime');
ws.onopen = function() {
    ws.send(JSON.stringify(    // 購読リクエストを送信
       "op": "subscribe",
       "args": [
            "trade:XBTUSD"
        ]
    }));
};
ws.onmessage = function(msg) { // メッセージ更新時のコールバック
    var response = JSON.parse(msg.data);
    response.data.forEach(function(data) {
        buf['BitMEX'][data.side === 'Buy' ? 0 : 1].push({
            x: data.timestamp, // タイムスタンプ
            y: data.price      // 価格(米ドル)
        });
    });
}

BitMEX

CoinCheck WebSocket API

最後は日本の取引所 CoinCheck です。対日本円のデータのみが配信されているので、それを表示してみましょう。リクエストとデータの形式は次の通りです。(CoinCheck WebSocket API の詳細は こちら

{
    "type": "subscribe",        // 購読リクエスト
    "channel": "btc_jpy-trades" // ビットコイン/日本円
}
[
    9856377,    // 取引 ID
    "btc_jpy",  // 通貨ペア
    "289544.0", // 価格
    "0.0367",   // 取引量
    "sell"      // 取引タイプ
]

コードとチャートは次の通りです。

buf['CoinCheck'] = [[], []];
var ws = new WebSocket('wss://ws-api.coincheck.com/');
ws.onopen = function() {
    ws.send(JSON.stringify({        // 購読リクエストを送信
        "type": "subscribe",
        "channel": "btc_jpy-trades"
    }));
};
ws.onmessage = function(msg) { // メッセージ更新時のコールバック
    var response = JSON.parse(msg.data);
    buf['CoinCheck'][response[4] === 'buy' ? 0 : 1].push({
        x: Date.now(), // タイムスタンプ(ミリ秒)
        y: response[2] // 価格(日本円)
    });
}

CoinCheck

これ以外にも、多くの取引所がリアルタイムデータを配信する API を提供しています。それぞれ微妙に形式が異なりますが、最小限の変更でチャートの表示ができますので、ご興味をお持ちの方は参考にしてください。