まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:canvas

Chart.js を使うと、レスポンシブ対応した各種グラフを簡単に実現できて便利です。便利でよく使うのですが、それ故にカスタマイズしたくなることも少なからず発生します。

例えばこんな例です。Chart.js を使うと水平の積み上げ棒グラフもこんな感じで作れるのですが・・・
2019111701

  :
  :

<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js"></script>

  :
  :

<script>
var data = {
  labels: [ 'カレーライス', 'ラーメン', 'ハンバーグ', 'サラダ' ],
  datasets: [{
    label: '脂質',
    data: [ 500, 300, 400, 100 ],
    backgroundColor: 'rgba( 255, 100, 100, 1 )'
  },
  {
    label: 'たんぱく質',
    data: [ 300, 250, 400, 200 ],
    backgroundColor: 'rgba( 100, 100, 255, 1 )'
  },
  {
    label: '炭水化物',
    data: [ 200, 300, 100, 50 ],
    backgroundColor: 'rgba( 100, 255, 100, 1 )'
  }],
};
var options = {
  scales: {
    title: {
      display: true,
      text: '三大栄養素別比較',
      padding: 3
    },
    xAxes: [{
      display: true,
      scaleLabel: {
        display: true,
        labelString: 'mg'
      },
      ticks: {
        min: 0
      },
      stacked: true  //. 積み上げ棒グラフ
    }],
    yAxes: [{
      display: true,
      scaleLabel: {
        display: true,
        labelString: ''
      },
      stacked: true  //. 積み上げ棒グラフ
    }]
  },
  legend: {
    labels: {
      boxWidth: 30,
      padding: 20
    },
    display: true
  },
  tooltips: {
    titleFontSize: 20,
    bodyFontSize: 20,
    mode: 'label'
  }
};

$(function(){
  var ctx1 = document.getElementById( 'myChart1' );
  var graph1 = {
    type: 'horizontalBar',
    data: data,
    options: options
  };
  var myChart1 = new Chart( ctx1, graph1 );
});
</script>

  :
  :

<canvas id="myChart1" style="position:relative; width:900; height:300;"></canvas>

  :
  :




このグラフの特定部分に追加の描画を加えたくなることがあります。例えばある値やエリアを強調表示するために塗りつぶしたい、といったカスタマイズです。カスタマイズ後の例としてはこんな感じ:
2019111702

  ↑横軸の 450 ~ 550 部分を薄くピンクで塗りつぶして強調


これを実現するには Chart.js のカスタマイズが必要になります。具体的にはヘルパー機能を使って水平棒グラフの draw() メソッドを拡張し、本来の draw() メソッド実行後に Canvas のグラフィックコンテキストを使って直線や矩形を追加で描画する、というカスタマイズを行います(変更箇所をで表記):
  :
  :

var options = {
  scales: {
    title: {
      display: true,
      text: '三大栄養素別比較',
      padding: 3
    },
    xAxes: [{
      display: true,
      scaleLabel: {
        display: true,
        labelString: 'mg'
      },
      ticks: {
        min: 0
      },
      stacked: true  //. 積み上げ棒グラフ
    }],
    yAxes: [{
      display: true,
      scaleLabel: {
        display: true,
        labelString: ''
      },
      stacked: true  //. 積み上げ棒グラフ
    }]
  },
  lineAtX1: 450,    //. この位置に矩形を描画
  lineAtX2: 550,    //. この位置に矩形を描画
  legend: {
    labels: {
      boxWidth: 30,
      padding: 20
    },
    display: true
  },
  tooltips: {
    //enabled: false,
    titleFontSize: 20,
    bodyFontSize: 20,
    mode: 'label'
  }
};


//. hozirontalBar を拡張
var originalLineDraw = Chart.controllers.horizontalBar.prototype.draw;
Chart.helpers.extend(Chart.controllers.horizontalBar.prototype, {
  draw: function () {
    //. 本来の hozizontalBar を描画
    originalLineDraw.apply(this, arguments);

    var chart = this.chart;
    var ctx = chart.chart.ctx;  //. グラフィックコンテキスト

    var lineAtX1 = chart.config.options.lineAtX1;
    var lineAtX2 = chart.config.options.lineAtX2;
    if( lineAtX1 && lineAtX2 ){
      var xaxis = chart.scales['x-axis-0'];
      var yaxis = chart.scales['y-axis-0'];

      //. 軸の値をグラフィックコンテキストの座標に変換
      var x1 = xaxis.getPixelForValue( lineAtX1 );
      var y1 = yaxis.top;

      var x2 = xaxis.getPixelForValue( lineAtX2 );
      var y2 = yaxis.bottom;

      ctx.save();
      ctx.beginPath();

      ctx.lineWidth = 5;
      ctx.fillStyle = 'rgba(255,200,200,0.1)';
      ctx.fillRect( x1, y1, x2 - x1, y2 - y1 );

      ctx.restore();
    }
  }
});

  :
  :


変に HTML5 Canvas に慣れていると、グラフィックコンテキストを取得して自由に描画して、・・・というカスタマイズを考えてしまうのですが、Chart.js でそのようにすると描画した部分を別のタイミングで Chart.js が上書きしてしまったりして、想定通りにカスタマイズできないことが多くあります。そのためか、上記のようなカスタマイズのためのヘルパーが用意されていて、その中でカスタマイズを行う、という手法を実装する必要があるようです。


Web アプリでドラッグ&ドロップ(以下 DnD と表記)すること自体はできるようになりました。HTML5 では DnD できる要素が多く定義されていたり、HTML5 以前にも mouseup イベントや mousedown イベントをハンドリングすることで独自に実装することは可能でした。 

ただモバイル Web で、つまりスマホのウェブブラウザから DnD を行うことはまだ困難が伴います。そもそもスマホの小さい画面で DnD を行うことが使いやすいかどうか、という根本的な問題もあると思っていて、それが理由かどうかは定かではありませんが、HTML5 の DnD API の多くはスマホブラウザからは使えないことが多いようです。

そういった事情を理解した上で、それでも現状でどこまでできるだろうか? という観点で実現方法を考えて実装し、公開してみました:
https://dotnsf.github.io/mobile_dnd_sample/


上記 URL にスマホのブラウザでアクセスすると以下のような画面になります。4角の枠内に5枚の付箋が貼られているイメージです:
2019111701


各付箋は指でドラッグして位置を変えることができるようにしています。下図はピンクの「ハロー」と書かれた付箋をドラッグして位置を変更した後の様子です:
2019111702


また各付箋はダブルタップすると編集モードになり、書かれた文字を編集することができるようになります。「ハロー」を編集して「ハロー!」にしてみました:
2019111703


編集モードで「OK」をタップすると編集後の文字列が反映されます:
2019111704


付箋を削除する場合は、一度ダブルタップして編集モードにして、「DELETE」をタップします:
2019111705


確認後に削除されて元の画面に戻ります。「abc」と書かれていた付箋が削除されました:
2019111706


新しい付箋を追加する場合は「NEW」をタップし、編集モードで内容を入力します:
2019111707


そして「OK」をタップすると新しい付箋が追加されます。この付箋も同様にドラッグして位置を変えたり、再度編集して内容を変更することができます:
2019111708



一連の機能紹介は以上です。一応スマホのウェブブラウザでもドラッグ&ドロップによる UI が実現できました。サンプルでは全て JavaScript を使ってフロントエンドだけで実現していますが、データベースとのバックエンド連携を加えることで永続化なども実現できると思っています。

詳しくは後述のソースコードを参照いただきたいのですが、今回のサンプルでは HTML5 Canvas を使って、Canvas 内に付箋に相当するオブジェクトを描画して実現しています。Canvas 内の Touch イベントを監視してドラッグを処理しています。また編集モード画面は Bootstrap のモーダルダイアログを使っています。


ソースコードはこちらです。実態は index.html ファイル1つで、全ての HTML と JavaScript がこの中に含まれています:
https://github.com/dotnsf/mobile_dnd_sample



このブログエントリの続きです:
Chart.js のクリックイベントハンドラ


JavaScript で各種グラフを便利・簡単につくれる Chart.js のカスタマイズとして、アイテムをクリックした時のイベントをハンドリングする方法を上記で紹介しました。今回はその応用として「クリックしたアイテムがどれかわかるように視覚化する」カスタマイズです。

具体例としてはこういう感じを想定しています。たとえば普通に棒グラフが描かれているとします:
2019110401


この左から2番目の棒をクリックしようとしているものとします。画面上ではマウスホバーまでしているので、左から2番目の棒の上にツールチップが表示されています:
2019110402


そしてこの左から2番目の棒をクリックした時に「ここがクリックされた」ことを明示することが目的です。下の例では左から2番目の棒のエリアを赤枠で囲って強調されるようにしています:
2019110403


具体的なコード例は以下になります。どの棒がクリックされたか、については click イベントハンドリングを行っていますが、そのあたりの詳細は前回のエントリを参照してください:
<html>
<head>
<meta charset="utf8"/>
<script type="text/javascript" src="//code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/2.2.1/Chart.min.js"></script>

<script>
$(function(){
  var data_labels = [ 0, 1, 2, 3, 4 ];
  var data = [ 1, 2, 4, 3, 0 ];

  var ctx1 = document.getElementById( 'myChart1' );
  var graph1 = {
    type: 'bar',
    data: {
      labels: data_labels,
      datasets: [{
        label: 'A',
        borderWidth: 1,
        backgroundColor: "#14c759",
        borderColor: "#14c759",
        data: data
      }]
    },
    options: {
      title: {
        display: true,
        text: 'クリックイベントハンドリング例',
        padding: 3
      },
      scales: {
        xAxes: [{
          categoryPercentage: 0.4
        }],
        yAxes: [{
          display: true,
          scaleLabel: {
            display: true,
            labelString: ''
          }
        }]
      },
      tooltips: {
        mode: 'label'
      }
    }
  };
  var myChart1 = new Chart( ctx1, graph1 );

  //. クリックイベント
  ctx1.addEventListener( 'click', function( event ){
    var item = myChart1.getElementAtEvent( event );
    if( item.length == 0 ){
      return;
    }
    item = item[0];

    var idx = item._index;        //. 左から何番目のアイテムがクリックされたか?
    var ctx = item._chart.ctx;    //. クリックされたアイテムのチャート描画部分のコンテキスト

//. ctx に対して、idx 番目のアイテムがある箇所を強調表示する。
//. 以下の例では idx 番目のアイテムがあるエリアを赤枠で囲って強調している

//. 描画する矩形の開始地点座標と、矩形の幅・高さを求める var x_right = item._xScale.chart.chartArea.right; var x_left = item._xScale.chart.chartArea.left; var x_width = ( x_right - x_left ) / data_labels.length; var y_height = item._yScale.height; var y_top = item._yScale.top; var draw_x0 = x_width * idx;
//. strokeRect 関数で対象エリアに矩形を描画する ctx.lineWidth = 5; ctx.strokeStyle = 'red'; ctx.strokeRect( draw_x0 + x_left, y_top, x_width, y_height );
}); }); </script> </head> <body> <div class="container"> <canvas id="myChart1" style="position:relative; width:800; height:200"></canvas> </div> </body> </html>

簡単に解説すると、クリックしたアイテムを item 変数に入れた後に item._chart.ctx を参照して(チャートが描画される Canvas の)コンテキスト変数を取得する、というのがミソです。これが取得できてしまえば、後はここに Canvas の機能や関数を使って自由に描画すればいいわけで、上の例では矩形を指定して色を付けるようにしています。



カスタマイズネタでテトリスを扱うことがあるのですが、その時のベースに使っているのがこちらです:
https://github.com/dionyziz/canvas-tetris

Dionysis Zindros さんが HTML5 Canvas で作ったテトリスです。Canvas 、JavaScript 、そして CSS でテトリスが再現されていて、MIT ライセンスで公開されています。実際に遊ぶ時にもブラウザで index.html ファイルを開くだけ(index.html ファイルは HTTP サーバー上にアップロードされている必要はなく、ローカルファイルとして開くだけでも可)で遊べます。

一応、遊び方にも触れておきます。index.html ファイルをブラウザで開くと以下のような画面になります。この「Play」ボタンをクリックするとテトリスがスタートします:
2019061703


操作はキーボードを使います。矢印の左右でテトリミノ(ピース)を左右に移動、上矢印で回転、下矢印で強制的に下に移動します:
2019061704



といった背景もあって実際に遊ぶのはもちろん、改良する場合においても技術的&ライセンス的なハードルが低く、デモやハンズオンも含めたセミナーのネタとして重宝しています。


そんなわけで、今日のブログエントリはこのテトリスを Obniz で操作する、というカスタマイズの紹介です。


Obniz (オブナイズ)をご存じない人のために簡単に紹介しておきます。Obniz は 2017 年に Kickstarter でクラウドファンディングが開始された IoT ボードです。Arduino や Raspberry Pi(Zero) と比較しても小型で、単体ではダイヤル式のスイッチと簡単な文字が表示できるディスプレイを入出力装置として持っています。これ以外に標準で無線 LAN および Bluetooth につながることができ(そのための設定をダイヤル式スイッチで指定します)、そして 12 個の GPIO によって各種センサーやモーターを接続することができます。MicroUSB からの給電により、(単体では)最大 1.8A の電流で稼働します。

そして最大の特徴といえるのが JavaScript による API の提供です。HTML や Node.js から利用可能な JavaScript でこれらの入出力装置の状態を監視したり、値を入力/設定/出力することができます。Obniz の JavaScript そのものの紹介が本ブログエントリの目的ではないので、詳しくは公式ドキュメントを参照してください。ググればネット上にサンプルを含めた使い方がたくさん見つかりますが、要するに「ウェブ(ページ)との連携が容易なボード」なのです:
https://obniz.io/doc/root





この Obniz ボードとの連携として、ボードのダイヤルスイッチでテトリミノを動かせるように改良しました。そのコードがこちらです:
https://github.com/dotnsf/canvas-tetris


こちらのコードをダウンロード(または git clone)して、Obniz を WiFi に接続後にブラウザで index.html ファイルを開いてください。すると最初に以下のような画面になって Obniz ID の指定を促されるので所有する Obniz の ID を指定します:
2019061702


そして "Play" ボタンをクリックでゲームスタートです。ここでは Obniz のダイヤルスイッチを使って、以下のように操作することができます:
ダイヤルスイッチ操作
左に回す左移動
右に回す右移動
押し込む回転


実際に遊んでいる様子がこちらです(下手すぎるのは撮影しながらだから、です・・・):



この仕組みのすごい所は「ゲーム画面とコントローラー(Obniz)が離れていても実現できる」点です。↑の動画では Obniz のある場所でゲーム画面を開いていますが、たとえばゲーム画面は地球の裏側で見られていたり、コンサート会場の大型スクリーンなどに表示されていてもいいわけです。そんな仕組みを簡単に実現できてしまっている、という点が素晴らしいと感じてます。

ちなみにカスタマイズの肝となっている箇所のコードはこちらです。js/controller.js ファイル内に以下のコードを追加して、Obniz スイッチの変更イベントを obniz.switch.onchange でハンドリングし、その内容に応じて対応するキーが押されたようにイベントを発生させています:
  :
//. obniz
var obniz = new Obniz("XXXX-XXXX"); //. <-- Edit with your own Obniz ID
obniz.onconnect = async function(){
  obniz.display.clear();
  obniz.display.print( "Obniz TETRIS" );

  obniz.switch.onchange = function( state ){
    obniz.display.clear();
    obniz.display.print( state );

    switch( state ){
    case 'left':
      keyPress( 'left' );
      render();
      break;
    case 'right':
      keyPress( 'right' );
      render();
      break;
    case 'push':
      keyPress( 'rotate' );
      render();
      break;
    default:
    }
  }
}

この Obniz の JavaScript SDK は本当に強力で、簡単にこういうことが実現できちゃいます。LED やセンサーを繋いだり、モーターを繋いだりしなくても、手軽にこんな使い方が実現できちゃうという、このポテンシャルの高さが魅力で気に入ってます。



先日、HTML5 Canvas のサーバーサイド版である Node-Canvas を紹介しました:
Node.js から使えるサーバーサイド Canvas : node-canvas


HTML5 Canvas と互換性のある API を使って動的に Canvas 上に描画をします。実際には単に描画をするだけでなく、描画した内容を画像として取り出したりすることで、サーバー側の JavaScript で動的に画像ファイルを生成することができるようになり、とても便利なライブラリです。

実際に使っていて、1点問題気になることがありました。開発中は(日本語環境バッチリな)自分のマシンを使って動作テストなどを行うので気が付きにくいのですが、実際の本番サーバー上で動かした際にキャンバス上に描画した日本語文字列が化けてしまうケースを発見しました。

例えばこのようなコードを実行して、サーバーサイドで画像を動的に生成したとします:
  :
var fs = require( 'fs' );
var Canvas = require( 'canvas' ),
    Image = Canvas.Image;

  :

var canvas = new Canvas( 300, 300 );
var ctx = canvas.getContext( '2d' );

//. 赤い直線を1本描画する
ctx.beginPath();
ctx.moveTo( 100, 100 );
ctx.lineTo( 200, 200 );
ctx.strokeStyle = 'red';
ctx.stroke();

//. 文字列を表示する
ctx.fillText( 'ハローワールド!', 10, 30 );

//. 画像データをファイルにして保存する
var b64 = canvas.toDataURL().split( ',' )[1];
var buf = new Buffer( b64, 'base64' );
fs.writeFileSync( __dirname + '/images/aaa.png', buf );

  :


このコードを Node.js で実行すると、./images/aaa.png というファイル名で動的に画像ファイルが生成されて保存されます。が、その時に生成される画像で「ハローワールド!」と明示されている文字列はサーバー実行環境によって正しく表示されたり、されなかったりします:

(正しく表示される場合)
2017052901

(文字化けを起こす場合)
2017052902


これはサーバー OS 側に日本語フォントが導入されているかどうかで決まります。特にクラウド環境においては OS は(デフォルトでは)英語版が用意されることが多く、そのまま利用しても日本語フォントが用意されていないため、「ハローワールド」という文字列を正しく描画できず、結果として文字化けを起こしてしまうことになります。

ということは、仮に文字化けを起こしても、OS に日本語フォントを導入した上でアプリケーションサーバーを再起動すれば回避できる、ということになります。

が、ここで問題が発生します。IBM Bluemix のような PaaS 環境の場合、OS 環境設定の自由度が低く、用意されたプラットフォームをカスタマイズして使うハードルが高くなります。現に SSH でログインすることはできますが apt-get などは使えません。またフォントファイルを後から無理やりコピーしてもアプリケーションサーバーを再起動するとそのファイルは消えてしまいます。このような自由度があまり高くないプラットフォーム上で日本語を正しく表示させることが結構難しかったりします。

そしてなんと更に、Node-Canvas の Issue ボードによると、この部分にはどうもバグがあるらしく、(2017/May/29 現在)ステータスが Open になっているので、まだ解決できていない模様でした:
https://github.com/Automattic/node-canvas/issues/783


・・・と、とても困った状況なのですが、結論から言うとなんとかアプリケーション側の工夫で回避することができました。まずは日本語フォントが必要になるので、フリーの日本語フォントを適当にダウンロードしてきます。例えば eFont プロジェクトから配布されているさざなみフォントの最新版を使うことにします。アーカイブファイル(*.tar.bz2)をダウンロード&展開し、sazanami-***.ttf というファイルをアプリケーション内の fonts/ ディレクトリに格納しておきます:
2017052903

(以下のコードではゴシックフォントしか使いませんが、とりあえず明朝フォントと合わせて展開しておきます)


そしてコードの該当部分を以下のように変更します:
  :
var fs = require( 'fs' );
var Canvas = require( 'canvas' ),
    Image = Canvas.Image;

  :

var canvas = new Canvas( 300, 300 );
var ctx = canvas.getContext( '2d' );

//. 赤い直線を1本描画する
ctx.beginPath();
ctx.moveTo( 100, 100 );
ctx.lineTo( 200, 200 );
ctx.strokeStyle = 'red';
ctx.stroke();

//. フォントを強制指定する
var Font = Canvas.Font;
var myFont = new Font( 'myFont', 'fonts/sazanami-gothic.ttf' );
ctx.addFont( myFont );
ctx.font = '20px myFont';

//. 文字列を表示する
ctx.fillText( 'ハローワールド!', 10, 30 );

//. 画像データをファイルにして保存する
var b64 = canvas.toDataURL().split( ',' )[1];
var buf = new Buffer( b64, 'base64' );
fs.writeFileSync( __dirname + '/images/aaa.png', buf );

  :


これで実行すると、フォントが指定のものに強制指定され、日本語が化けずに表示できるようになります(下図は IBM Bluemix 環境下で実行した結果です):
2017052904


なお、上記の方法を使う場合、Ubuntu はこのままでも動くのですが、Mac OS X 環境においては Pango ライブラリを無効にした上で実行する必要があります。具体的には Node-Canvas を npm install した後で、node_modules/canvas/binding.gyp ファイルを編集し、15行目の値を 'false' にする(8行目と同じにする)作業を行い、再度 npm rebuild した上で node コマンドを実行する必要がありました。ご注意ください。


このページのトップヘ