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

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

タグ:canvas

HTML5 Canvas を使って1年間かけて一周する「年間時計」を作ってみました:
https://dotnsf.github.io/yearclock/
2019112300


表示すると1分で一周する秒針と、1年で一周する年針(?)が表示されます。年針は針というよりはパイチャートの要領で面積で表示されます。ちなみに上図は 11 月後半に表示したものです。もうかなり進んでいますね。。

仕組みはそれほど複雑ではなく、HTML5 Canvas 内に円と弧を描画した上で秒針を setInterval() を使って1秒おきに描画しています。画像の透明度をうまく使って秒針の残像が残る形にしています。パックマン型に弧を描画する技術については先日公開したこちらのエントリを参照ください:
Canvas でパックマン型に塗りつぶした弧を描く


計算上では(うるう年でない年は)毎年 11月25日 の正午が一年間の 90% を経過する瞬間になります。今年も残すはあと1割!

HTML5 の Canvas を使うことで HTML の画面内にコンテキストを利用した図形を比較的自由に描くことができるようになります。この機能の1つである arc() 関数を使うと、下図のような「円の弧」を描画できます(塗りつぶすかどうかは選択できます):
2019112203


この弧を描く際のコードは以下のようなものです:
  :
<script>
  var canvas1 = document.getElementById( 'myCanvas1' );
  if( !canvas1 || !canvas1.getContext ){
    return false;
  }
  var ctx1 = canvas1.getContext( '2d' );

  //. 円の情報
  var r = 80;
  var x0 = 100;
  var y0 = 100;

  var deg = 190;
  ctx1.beginPath();
  ctx1.arc( x0, y0, r, -90 * Math.PI / 180, deg * Math.PI / 180, false );
  ctx1.fillStyle = "rgba( 255, 128, 128, 0.8 )";
  ctx1.fill();
</script>

<body>
<canvas id="myCanvas1"></canvas>
</body>
  :

で、このような図形を描くのが目的であればいいのですが、パックマンのような(円からピザの一部を切り取ったような)画像を描きたい場合はこれでは目的の画像とは異なります。なんとかしてパックマン型に塗りつぶした弧を描く方法はないでしょうか? というのが今回のテーマです。





2019112202


結論としては可能で、「arc() 関数の実行直後に円中心に向かって直線を引く」のがその答えになります:

  :
<script>
  var canvas1 = document.getElementById( 'myCanvas1' );
  if( !canvas1 || !canvas1.getContext ){
    return false;
  }
  var ctx1 = canvas1.getContext( '2d' );

  //. 円の情報
  var r = 80;
  var x0 = 100;
  var y0 = 100;

  var deg = 190;
  ctx1.beginPath();
  ctx1.arc( x0, y0, r, -90 * Math.PI / 180, deg * Math.PI / 180, false );
  ctx1.lineTo( x0, y0 );
  ctx1.fillStyle = "rgba( 255, 128, 128, 0.8 )";
  ctx1.fill();
</script>

<body>
<canvas id="myCanvas1"></canvas>
</body>
  :

これでパックマンが描画できました:
2019112200

 

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 の機能や関数を使って自由に描画すればいいわけで、上の例では矩形を指定して色を付けるようにしています。



このページのトップヘ