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

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

タグ:canvas

ほぼ備忘録目的のブログエントリです。

HTML で <canvas> を使うと画面に単純な画像だけでなく、グラフィックコンテキストを使って自由度の高い図形や関数曲線を描くことができるようになります。個人的にもよく使っています。

特筆すべきはピクセル単位での画素情報を取得することもできる点です。ここでの画素情報とは RGB 値や輝度の値といった情報です。例えば <canvas id="mycanvas"></canvas> 内に描かれている内容のピクセル単位での画素情報を動的に取得するには以下のような JavaScript コードを実装すれば可能です:
  var canvas = document.getElementById( 'mycanvas' );
  if( !canvas || !canvas.getContext ){
    return false;
  }
  var ctx = canvas.getContext( '2d' );

  var imagedata = ctx.getImageData( 0, 0, ctx.canvas.width, ctx.canvas.height );
  for( var i = 0; i < imagedata.height; i ++ ){
    for( var j = 0; j < imagedata.width; j ++ ){
      var idx = ( j + i * imagedata.width ) * 4;

      var r = imagedata.data[idx];    //. R(0-255)
      var g = imagedata.data[idx+1];  //. G(0-255)
      var b = imagedata.data[idx+2];  //. B(0-255)
      var a = imagedata.data[idx+3];  //. 輝度(0-255)

        :
    }
  }

この中で画素情報を取得する際に使っているのが getImageData() 関数です。これは canvas 内の開始点座標と幅、高さを指定し、その指定範囲内の画素情報を配列で取り出すことができます。また上述例のように1つのピクセルの情報は配列内の4つの値(R, G, B, 輝度)として格納されており、1つずつ取り出して参照することも可能です。画像を独自に減色したりする際等に必要な情報を取り出すことができて、個人的にも便利に使っています。


このように便利な canvas と getImageData() 関数ですが、この getImageData() 実行時に以下のようなエラーが発生して、画素情報を取得できないケースがありました:
'The canvas has been tainted by cross-origin data.'

ん? "Cross-Origin" ?? どういうこと???


これが発生した時の <canvas> 内は、事前に以下のような処理を行って画像が表示されていました:
  var img = new Image();
  img.src = 'https://www.xxx.com/images/abc.png';
  img.addEventListener( 'load', function(){
    var canvas = document.getElementById( 'mycanvas' );
    if( !canvas || !canvas.getContext ){
      return false;
    }
    var ctx = canvas.getContext( '2d' );
    ctx.drawImage( img, 0, 0, canvas.width, canvas.height );
  }, false );

要するに <canvas> 内には https://www.xxx.com/images/abc.png という(架空の) URL で表現される画像ファイルが表示されている状態でした。単に画像を表示するだけなら <img src="https://www.xxx.com/images/abc.png"/> みたいに記述すればいいのですが、後で前述のような画素情報を取り出す処理を実行したかったので、<img> ではなく <canvas> を使って表示していたのでした、という事情がありました。が、このケースだと getImageData() 実行時に上述のようなクロスオリジン関連のエラーが発生して情報を取得することができなくなっていました。これはいったい・・?


エラーメッセージからなんとなく想像はできたのですが、この getImageData() 関数はクロスオリジンの画像が表示されている場合は CORS の制約がかかるようです。つまりこの例であれば www.xxx.com というサーバー上にある https://www.xxx.com/images/abc.png という画像を表示しています(上のコード例では drawImage() 関数によって実際に表示されます)。この drawImage() 関数はクロスオリジンに関係なく実行することができるので、(自サーバーではない)www.xxx.com というサーバー上にある画像に対して実行してもエラーが発生することなく実行され、画像は <canvas> 上に表示されます。

しかし getImageData() 関数についてはクロスオリジン制約があるようです。したがって自サーバー上ではない URL を指定して表示された画像に対して実行すると、今回のようなエラーが発生してしまう、という制約があるようでした。


このエラーを回避しようとすると、www.xxx.com 側で CORS の制約を緩和するような設定を行うか、それが難しい場合は目的の画像をクロスオリジンにならない形の URL で指定できるようにする必要があります(そのため静的な HTML や JavaScript だけだと実現が難しいです)。具体的にはサーバーサイドの処理で、例えば GET /getimage のような REST API を用意し、この API が目的の画像バイナリと Content-Type を返すようにする、といった対応が必要になります。


現にこういう方法で回避できちゃう CORS 制約は、処理が面倒くさくなるだけであまり意味ないと思っちゃうんだけどな。。


 

かなり自分向けですが、タイトル通りのウェブサービスを作ってみました。


【背景】
まずこのサービスを作った背景です。COVID-19 による在宅勤務やリモート会議が主流となる中で、ZOOM や WebEx などのウェブ会議サービスが多く使われるようになりました。このウェブ会議、サービスによって機能の違いはありますが、概ね以下のような機能を持っています:
・PC やスマホを使い、インターネット経由で仮想的な会議室に入って会議する
・音声は全員でリアルタイムに共有する
フロントカメラを使うことで参加者全員の表情を確認しながら進めることができる
ホスト役の PC 画面を全員で共有しながら会議できる(画面を共有する PC は後から変更可)

video_kaigi


これらの特徴によって、情報共有や伝達事項など「通知」目的のリモート会議であれば目的はほぼ達成できると思っています。通知したい人の画面を使ってプレゼンテーションを行い、その様子を他の参加者が音声と一緒にリアルタイムに確認し、質問なども行うことができるからです。
2020042601


一方、この形式ではまだリモート開催が難しい会議形態もあります。情報共有や伝達事項ではなく、ハンズオントレーニングなど「参加型」目的の場合です。ホスト役の人の画面を使ってハンズオン資料や作業内容を共有するところまでは何も変わりませんし、その様子を見ながら参加者が何の問題もなく作業を進めていける場合や、問題が発生しても自身で解決できる場合は問題になることもありません。Google Drive や Visual Studio Code Live Share のような仕組みを使うことで、全員で1つのファイルを同時に編集することはできます。この形がとれる場合においては音声と併用することでリモートサポートも可能になる範囲だと思っています。

ただローカル PC を使って作業する場合、特に参加者のスキルが必ずしも充分でない場合や、そうでなくても想定外のエラーが発生してしまった場合など、ウェブ会議では解決が途端に難しくなってしまいます。オンラインのウェブ会議ではなく、一箇所に(物理的に)集まって行う形式であれば、その場で本人の画面を覗き込めばエラー内容を確認できます。そのエラー内容が入力ミスなどのケアレスミスであれば正しい入力内容を指摘してあげるだけで解決できます。一方、入力内容が正しいのに想定外のエラーが発生しているようであれば、エラーメッセージやログ内容を参考にググったりして調べることになると思うのですが、画面が見えない限り、その場合分けすらも難しいという問題があります。また「エラーメッセージやログ内容を参考にググったり・・」と書きましたが、ウェブ会議で参加者のエラーメッセージやログ内容を正確に知るのは(その参加者のスキルレベルにもよりますが)かなり面倒です。実際にはエラー画面を共有してもらえればいいのですが、参加者がその手順を理解しているとは限りませんし、PC 画面を参加者全員に知られることに抵抗を感じる人もいると思われます。またその作業中は自分の(ホストの)画面を共有できなくなるので他の参加者の作業が止まってしまう可能性も危惧する必要があります。

更には、そもそもまったくセミナーに付いてこれない人がいて、序盤で躓いたまま全然進められていなかったりした場合でも、ウェブ会議のホスト側ではそのような人が存在していることに気付くことすらかなり困難です。
2020042602


比較的初心者を対象とするようなハンズオン勉強会を行うケースなど、上述のようなケースが想定されるオンライン会議をスムーズに行うには既存のウェブ会議システムだけでは難しいのではないか、、と考えていました。


【解決策の仮説】
上述のようなケースを解決するにはどうすればよいか? この問に対する明確な解答ではないのですが、1つ仮説を考えました:
従来のウェブ会議の逆を行うシステムがあれば解決するのではないか?

つまりホストの画面を参加者全員で共有するのではなく、参加者全員の画面の様子をホストが見れるようになっていれば、参加者側の操作でエラーが起こってもホストからエラーメッセージを確認できるし、作業についていけなくなったとしてもその様子をホスト側が確認できるのではないか?

この内容はあくまで仮説ですが、理屈の上では問題が解決しそうな気もしています。 この仮説を検証するための仕組みとして、ウェブ会議の逆(ホスト以外の参加者全員の画面をホストと共有する)を実現できる検証サービスを作ってみました。まだ使い勝手に難があることも理解していますが、仮説を検証するための機能までは実装できていると思っていて、特別なアプリケーションのインストールも必要としない形で実現できているので検証用に公開します。


【作ったサービス】
公開している URL は以下で紹介しますが、オープンソースとしても公開しています。ソースコードはこちらです:
https://github.com/dotnsf/screen_collect

また、このサービスを利用するには Windows または macOS で比較的最近の Chrome ブラウザを利用している必要があります。後述する MediaDevices インターフェースを使って実装している関係で、このインターフェースをサポートしていないスマホやタブレットのブラウザでは動きません。また他のブラウザでは未検証です。

以下、使い方を紹介します。


【使い方】
最初に、このサービスは実行順を守って動かす必要があります。最初にホスト側が会議室を準備し、その準備後にゲストが参加用 URL にアクセスすることで正しく実行されます(ホストが会議室を準備する前にゲストが URL にアクセスしてもその画面は共有対象になりません)。

まずホスト役の人が対応ブラウザを使って以下の URL にアクセスします(なおこのデモ用サービスでは Basic 認証は ID: admin, PW: password です):
https://screen-collect.mybluemix.net/view?room=XXXXXXXX

XXXXXXXX 部分は会議 ID のようなもので、ユニークな文字列を指定する必要があります(他の人と同じ値を使うと挙動がおかしくなります)。例えば日付や自分の名前を使って、以下の例のような URL を指定してください:
https://screen-collect.mybluemix.net/view?room=dotnsf-20200426

この時点では正しくアクセスできていても画面には何も表示されません(こんな感じの画面になれば正しくアクセスできています):
2020042601


この画面を使ってゲストの画面を共有することになるので、この画面を消さずに残しておいてください。またホストはこの画面をリロードしないよう気をつけてください(上述の順序の都合で、ホスト側ウィンドウをリロードした場合はゲスト側ウィンドウもリロードしてもらう必要があります):

最後にホストからゲストに参加用の URL を(メールやメッセンジャー等で)知らせる必要があります。ゲストへは上記 URL から "view" を除いた文字列を指定して通知してください。上記例の場合だと、この URL を教えることになります:
https://screen-collect.mybluemix.net/?room=dotnsf-20200426


ゲスト全員へ参加用 URL を通知し終わったらホスト側の準備は完了です。

ホスト側の準備が完了後にゲストが通知された URL にアクセスします(ゲストは Basic 認証なしでアクセス可能です)。なお試験的に一台の PC でホストとゲスト両方の画面をテストしたい場合は MediaDevice インターフェースのサポートの都合もあり、ホスト画面を FireFox で、ゲスト画面を Chrome で表示するようにしてください。

するとゲストには以下のような画面が表示されます:
2020042601


Room には URL で指定された会議 ID が表示されています。もしホストが URL を間違えて通知していても、ここで会議 ID を正しく修正して続行することで以下正しく実行されます。

そして Your name .. と書かれたフィールドに参加者の名前(本名でもニックネームでも可。ホストの人がわかる名前)を入力して Start ボタンをクリックします。
2020042602


すると画面には入力した名前が表示されますが、同時に別ウィンドウで画面共有ダイアログが表示されます。ここでゲストは共有範囲を選択します。共有範囲はアプリケーションウィンドウの単位でも、Chrome ブラウザのタブの単位でも、また全画面を選択することも可能です(以下の例では全画面を選択しています):
2020042603


共有範囲を選択したら「共有」ボタンをクリックします:
2020042604


するとゲスト画面では画面共有ダイアログが消え、代わりに小さな共有ウィンドウが表示されます(ゲストはこのウィンドウ画面を消さないよう注意してください):
2020042605


これでゲスト側の準備も完了しました。この後、ゲストは自由にアプリケーションを使ってください(下図ではゲストが Visual Studio Code を起動してプログラミングしている様子にしています):
2020042606


(実際のオンライン勉強会などではホストとゲストが事前にここまで行った上で実際のハンズオン作業に入ってもらう想定です)


ここで改めてホスト側の画面を確認します。さっきまで何も表示されていなかったホスト画面でしたが、ゲストが共有設定を完了すると、ゲストの名前とその画面がパネル表示されているはずです。またゲストの画面は(このデモサービスでは)5秒おきに更新されます:
2020042602


更に別のゲストが共有設定を完了するとパネルが追加され、以下のような感じになります。ホストの1画面内にゲストの画面が追加されて一覧できるようになり、(5秒おきですが)そこで行われているオペレーションの内容も確認できます。パネルはゲストの人数に応じて(共有設定が完了するたびに)追加されていきます:
2020042603


パネル内画面は縮小表示されていますが、画像を選択してクリックするとゲストのスクリーンと同じ縦横比率で拡大して表示されます。これでゲスト側でエラーメッセージなどが表示されていてもホスト側から視認することができるようになりますし、オンラインセミナーに付いてこれなくなって遅れ気味の人もホスト側から確認できるようになる、と思っています:
2020042604


ゲスト側は Chrome のゲストウィンドウを消すか、以下のダイアログの「共有を停止」をクリックすることで画面共有を停止できます:
2020042607


以上がサービスの使い方です。特別なアプリケーションをインストールすることなく、ブラウザだけで複数ユーザーのスクリーン内容をホストに共有する、という「ありそうでなかった※」サービスだと思っています。

※参加者の表情をカメラで映して同時に共有する、という機能は多くのウェブ会議システムに装備されていますが、参加者のスクリーンを共有する機能はなぜか付属していない機能だと思っています。

今後リモート勉強会などであらかじめこの準備をしておくことで、トラブルシューティングやリモートサポートに活用できないかと考えています。また上述のようにソースコードも公開しているので、興味ある方は専用環境を構築してお使いいただいても構いません。

以下、実現方法に関する技術的な補足説明です。興味ある方は引き続きどうぞ。


【どうやって実現しているか】
まず、このサービスはホストもゲストもウェブブラウザだけで(アプリケーションのインストールなしで)実現しています。また HTTP(s) 以外のプロトコルは使っていません。したがって比較的多くの環境で利用できるようになっていると思っています。

で、これを実現している鍵というかキーワードが何度か上述されている MediaDevices インターフェースです。簡単にいうと HTML と JavaScript だけで画面共有とそのために必要な UI を実現する技術です。ただ比較的新しい技術である上に対応している OS やブラウザはかなり少なく、スマホ・タブレット系は 2020/04/26 時点では全滅といっていい状況です(まあスマホでハンズオンセミナーに参加する人も少ないとは思うけど・・・)。MediaDevices インターフェースについては過去にこれをテーマにブログで紹介したこともあるので、詳しくはそちらも参照してください(この時はホスト・ゲストに分けず、1つの HTML 画面内で画面ストリームを取得してそのまま表示する、という内容でした):
MediaDevices インターフェースを使った画面共有

今回上記で紹介したデモはこの応用といえるもので「ゲストで取得した画面ストリームの内容を使い、バックグラウンドで5秒おきにスクリーンショットを撮ってホストに画像をポストし、ホストで受け取った画像を表示する」ということをゲストブラウザ内の JavaScript とホストサーバーの WebSocket を使って実現しています。機能的にはわざわざスクリーンショット(画像)に変換せず、ストリームのまま(動画のまま)ホストに送信する選択肢もあるのですが、さすがに帯域的にもホストの処理内容的にも(そしてデモサーバーの処理能力的にも)厳しそうな感触があったので、意図的に5秒おきにスクリーンショットを送受信する形にしています(5秒という部分は別途専用環境を作る際には変更可能です)。ただこの結果、ネットワーク上のすべてのやり取りを HTTP(s) と(HTTP を拡張した)WebSocket に集約することができ、帯域問題に加えてファイアウォール的な制約をすり抜けやすい形で実現できたという副産物もあります。

もう少し詳しく2点説明します。まずゲスト側で(JavaScript だけで)共有範囲のスクリーンショットを撮る、という操作ですが、これは上述の MediaDevices インターフェースを使って共有アプリケーション画面のストリームを取得した後に、以下のような処理を行っています。

"Your name .." で指定した名前だけが表示されているように見える HTML 画面には、実は非表示属性の付いた <video> 要素と、同じく非表示属性の付いた <canvas> が存在しています:
<body>
  <h3 id="name"><%= name %></h3>

  <!-- Video element (live stream) -->
  <div class="hidetop">
    <video autoplay playsinline id="video" width="640" height="480"></video>
  </div>

  <!-- Canvas element (screenshot) -->
  <div class="hide">
    <canvas id="canvas" width="640" height="480"></canvas>
  </div>
</body>

↑(hidetop クラスと hide クラスでブロックごと非表示指定されているので、ゲストユーザーからは何も見えません)

そして5秒おきに以下の JavaScript 関数 : video2image() が実行されます:
function video2image(){
  if( localStream ){
    var canvas = document.getElementById( 'canvas' );
    var ctx = canvas.getContext( '2d' );

    var video = document.getElementById( 'video' );
    var w = video.offsetWidth;
    var h = video.offsetHeight;

    canvas.setAttribute( "width", w );
    canvas.setAttribute( "height", h );

    ctx.drawImage( video, 0, 0, w, h );
    var png = canvas.toDataURL( 'image/png' );

    //. 画像を通知
    var msg = {
      uuid: uuid,
      room: '<%= room %>',
      image_src: png
    };
    socketio.json.emit( 'image_client', msg );
  }
}

localStream がスクリーン画面のストリームです。このストリームは <video> 要素と接続しています(この辺りは上記のブログの内容を参照ください)。そして <video> の画面を(非表示の) <canvas> にコピーすることでスクリーンショットを取得し、その内容を canvas.toDataURL() で画像データに変換した上で、Socket.IO を使い、ゲストの ID と一緒に WebSocket でホストの image_client イベントに送信しています。これによりバックグラウンド処理で5秒おきにスクリーンショット画像をホストに送る、という処理が実現できています。

そしてホスト側にはスクリーンショット画像を WebSocket で受け取った後(image_client イベント発生時の)の処理が記述されています:
  socketio.on( 'image_client_view', function( msg ){
    var socket_id = msg.uuid; //msg.socket_id;
    if( socket_ids.indexOf( socket_id ) > -1 ){  //. socket_ids == [];
      $('#image_'+socket_id).prop( 'src', msg.image_src );
      $('#image_'+socket_id).prop( 'title', msg.comment );
    }
  });

メッセージの ID でどのゲストからのイベントであるかを識別した上で、その中の image_src を使って画像情報(=スクリーンショット)を取り出し、ホスト画面内の適切な箇所の画像(=対象ゲストのパネル内の画像)を動的に再描画しています。

実際にはこれらの抜粋処理以外にも、これらをつなぐための処理や初期化などをアプリケーション・サーバー側で行ったりしています。詳しい内容については公開しているソースコードを参照ください。大規模な実験を行うようなケースでは別途独自の環境を準備した上で行っていただけると助かります:
https://github.com/dotnsf/screen_collect


【最後に】
こういったサービスを作って試すことで「こういうケースでは使えそう、こういうケースだとまだ○○の問題がある」みたいな形で次のステップが見えてくると思っています。自分的にはある程度のスキルを持った参加者による「もくもく会」のような内容であれば使えそうだという感触を持っていますが、より初心者参加型に近い内容だと、これだけで充分なサポートができるかどうかは不安もあります。

ただそれらが見えてくることで改良点のヒントにもなるだろうし、用途や条件によっては実践で使うこともできるかもしれないと考えています。またこのデモサービスを使ってみていただいた感想や「こういうケースではOKだった/NGだった」のようなフィードバックもいただけると嬉しいです。いずれにせよオンライン勉強会の体験改善は自分にとっても無視できないテーマなので、こういった形で解決できることがあれば引き続き取り組んでいきたいと思っています。


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 が上書きしてしまったりして、想定通りにカスタマイズできないことが多くあります。そのためか、上記のようなカスタマイズのためのヘルパーが用意されていて、その中でカスタマイズを行う、という手法を実装する必要があるようです。


このページのトップヘ