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

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

2019/10

公私で Chart.js をよく使っています。D3.js と並ぶビジュアライゼーションライブラリですが、各種グラフを描く、という目的であれば Chart.js の方が使いやすいと思っています:
2019101600


この Chart.js を使って、例えば積み上げ式の棒グラフを描くのであれば、こんな感じで記述します。HTML5 Canvas と JavaScript を使ってデータやオプションを指定して描画します:
<html>
<head>
<meta charset="utf8"/>
<script type="text/javascript" src="//code.jquery.com/jquery-2.0.3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/2.2.1/Chart.min.js"></script>
<script>
$(function(){
  //. 乱数を使ってデータを生成
  var label_data = [];
  var red_data = [];
  var green_data = [];
  var blue_data = [];
  for( var i = 0; i < 10; i ++ ){
    label_data.push( i );
    red_data.push( Math.floor( Math.random() * 100 ) );
    green_data.push( Math.floor( Math.random() * 100 ) );
    blue_data.push( Math.floor( Math.random() * 100 ) );
  }

  var ctx = document.getElementById( 'myChart' );
  var myChart = new Chart( ctx, {
    type: 'bar',
    data: {
      labels: label_data,
      datasets: [{
        label: 'Red',
        borderWidth: 1,
        backgroundColor: "#ffaaaa",
        borderColor: "#ff5555",
        data: red_data
      },
      {
        label: 'Green',
        borderWidth: 1,
        backgroundColor: "#aaffaa",
        borderColor: "#55ff55",
        data: green_data
      },{
        label: 'Blue',
        borderWidth: 1,
        backgroundColor: "#aaaaff",
        borderColor: "#5555ff",
        data: blue_data
      }]
    },
    options: {
      title: {
        display: true,
        text: '3色',
        padding: 3
      },
      scales: {
        xAxes: [{
          stacked: true,
          categoryPercentage: 0.4
        }],
        yAxes: [{
          stacked: true
        }]
      },
      legend: {
        labels: {
          boxWidth: 30,
          padding: 20
        },
        display: true
      },
      tooltips: {
        mode: 'label'
      }
    }
  });
});
</script>
</head>
<body>
<canvas id="myChart" style="position:relative; width:800; height:200;"></canvas>
</body>
</html>



このページをウェブブラウザで表示すると以下のようになります。単にグラフが表示されるだけではなく、マウスオーバー時のインタラクティブな処理なども自動的に行われます:

2019101601


また canvas のコンテキスト要素を使うことで Chart.js のチャートに各データ(棒グラフであれば棒の部分)をクリックした時のイベントハンドリングを実装することも可能です:
<html>
<head>
<meta charset="utf8"/>
<script type="text/javascript" src="//code.jquery.com/jquery-2.0.3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/2.2.1/Chart.min.js"></script>
<script>
$(function(){
  //. 乱数を使ってデータを生成
  var label_data = [];
  var red_data = [];
  var green_data = [];
  var blue_data = [];
  for( var i = 0; i < 10; i ++ ){
    label_data.push( i );
    red_data.push( Math.floor( Math.random() * 100 ) );
    green_data.push( Math.floor( Math.random() * 100 ) );
    blue_data.push( Math.floor( Math.random() * 100 ) );
  }

  var ctx = document.getElementById( 'myChart' );
  var myChart = new Chart( ctx, {
    type: 'bar',
    data: {
      labels: label_data,
      datasets: [{
        label: 'Red',
        borderWidth: 1,
        backgroundColor: "#ffaaaa",
        borderColor: "#ff5555",
        data: red_data
      },
      {
        label: 'Green',
        borderWidth: 1,
        backgroundColor: "#aaffaa",
        borderColor: "#55ff55",
        data: green_data
      },{
        label: 'Blue',
        borderWidth: 1,
        backgroundColor: "#aaaaff",
        borderColor: "#5555ff",
        data: blue_data
      }]
    },
    options: {
      title: {
        display: true,
        text: '3色',
        padding: 3
      },
      scales: {
        xAxes: [{
          stacked: true,
          categoryPercentage: 0.4
        }],
        yAxes: [{
          stacked: true
        }]
      },
      legend: {
        labels: {
          boxWidth: 30,
          padding: 20
        },
        display: true
      },
      tooltips: {
        mode: 'label'
      }
    }
  });

  //. クリックイベントハンドラー
  ctx.addEventListener( 'click', function( evt ){
    var item = myChart.getElementByEvent( evt );
    var item = myChart.getElementAtEvent( evt );
    if( item.length == 0 ){
      return;
    }

    item = item[0];  //. クリックしたオブジェクトデータ(棒)
  });
});
</script>
</head>
<body>
<canvas id="myChart" style="position:relative; width:800; height:200;"></canvas>
</body>
</html>

で、上記で取得した item というのが「クリックされたオブジェクト」となり、上記のような方法で取得できるのですが、意外と難しかったのが「配列の何番目のデータがクリックされたのか?」を調べる方法でした。上記の方法でクリックされたオブジェクトそのものを取得することはできますし、その中の数値などを取り出すこともできますが、配列の何番目のデータがクリックされたのか? という情報を取得するのに手間取ってしまいまいました。

この辺り、業務で Chart.js を使う機会があったのでそのタイミングで調べてみました。答はこんな方法でした:
<html>
<head>
<meta charset="utf8"/>
<script type="text/javascript" src="//code.jquery.com/jquery-2.0.3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/2.2.1/Chart.min.js"></script>
<script>
$(function(){
  //. 乱数を使ってデータを生成
  var label_data = [];
  var red_data = [];
  var green_data = [];
  var blue_data = [];
  for( var i = 0; i < 10; i ++ ){
    label_data.push( i );
    red_data.push( Math.floor( Math.random() * 100 ) );
    green_data.push( Math.floor( Math.random() * 100 ) );
    blue_data.push( Math.floor( Math.random() * 100 ) );
  }

  var ctx = document.getElementById( 'myChart' );
  var myChart = new Chart( ctx, {
    type: 'bar',
    data: {
      labels: label_data,
      datasets: [{
        label: 'Red',
        borderWidth: 1,
        backgroundColor: "#ffaaaa",
        borderColor: "#ff5555",
        data: red_data
      },
      {
        label: 'Green',
        borderWidth: 1,
        backgroundColor: "#aaffaa",
        borderColor: "#55ff55",
        data: green_data
      },{
        label: 'Blue',
        borderWidth: 1,
        backgroundColor: "#aaaaff",
        borderColor: "#5555ff",
        data: blue_data
      }]
    },
    options: {
      title: {
        display: true,
        text: '3色',
        padding: 3
      },
      scales: {
        xAxes: [{
          stacked: true,
          categoryPercentage: 0.4
        }],
        yAxes: [{
          stacked: true
        }]
      },
      legend: {
        labels: {
          boxWidth: 30,
          padding: 20
        },
        display: true
      },
      tooltips: {
        mode: 'label'
      }
    }
  });

  //. クリックイベントハンドラー
  ctx.addEventListener( 'click', function( evt ){
    var item = myChart.getElementByEvent( evt );
    var item = myChart.getElementAtEvent( evt );
    if( item.length == 0 ){
      return;
    }

    item = item[0];
    var index = item._index;  //. 配列の何番目のデータがクリックされたか
    var item_data = item._chart.config.data.datasets;  //. クリックされたオブジェクトのデータセット
    console.log( index );
    console.log( item_data );
  });
});
</script>
</head>
<body>
<canvas id="myChart" style="position:relative; width:800; height:200;"></canvas>
</body>
</html>

最初はこの方法がわからず、クリックイベントのマウス位置やオフセット位置から計算しようとしていたのですが、これだとブラウザごとの違いを意識する必要があり、面倒なコードになってしまっていました。

結論としては上記の item オブジェクトの中の _index という属性に隠れていたようです。ちなみに最初のオブジェクトは 0、2番目のオブジェクトの場合は 1 になるようです。


(追記 2020/04/10)
誤字のご指摘があり、getElementByEvent を getElementAtEvent に修正しました。
(追記終わり)




最近の PDF ファイルはテキスト情報が含まれています。そのため PDF 内をテキスト検索することができるようになっています:

このテキスト部分だけを機械的に抜き出す、というのが今回紹介するテーマです。具体的には Node.js を使って PDF からテキスト情報を抜き出すプログラムを実装してみました。

Node.js の場合、PDF を扱うことのできるライブラリがいくつか存在していますが、今回はこれを使いました。日本語を含むテキストであっても正しく取り出すことができそうでした:
pdf-parse

2019101201


また今回はこの公開 PDF からテキストを取り出してみます:
Kubernetesの基礎

2019101202


後述のコードでは対象 PDF ファイル名を指定して実行できるようにしています。そのため、上記リンク先から同 PDF (h2-takara-3.pdf)をダウンロードしておいてください。これ以外の PDF を使って試すこともできますが、その場合もあらかじめダウンロードしておいてください。

そして具体的なコード(pdf-sample.js)は以下のようになります:
//. pdf-sample.js
var fs = require( 'fs' ),
    pdf = require( 'pdf-parse' );

if( process.argv.length > 1 ){
  var filename = process.argv[2];
  var buf = fs.readFileSync( filename );
  pdf( buf ).then( function( data ){
    var text = data.text;
    console.log( text );
  }).catch( function( err ){
    console.log( err );
  });
}else{
  console.log( 'Usage: $ node pdf-sample ' );
}

pdf-parse を使っている部分を赤字にしました。エラー処理含めてこれだけです。

では実際に実行してみます。まず実行前の準備として pdf-parse ライブラリをインストールしておきます:
$ npm install pdf-parse


そして、対象 PDF ファイル名をパラメータ指定して node コマンドで実行します。例えばプログラムコード(pdf-sample.js)と同じフォルダに PDF ファイル(h2-takara-3.pdf)を保存した場合であれば、以下のように指定して実行します:
$ node pdf-sample ./h2-takara-3.pdf

正しく実行されると、以下のようにテキスト抽出結果がコンソール画面に表示されます:
$ node pdf-sample ./h2-takara-3.pdf


Kubernetesの基礎
InternetWeek 2018
2018年11月28日
日本アイ・ビー・エム株式会社
クラウド事業本部 高良真穂

発音/略称/Logo
綴りKubernetes
発音
koo-ber-net-ees
略称K8s
Kubernetes
12345678
クーベネティスのロゴ

K8sは一言で何ができる?
K8sは、コンテナのアプリ運用のためのOSS
1.コンテナの組み合わせ利用
2.スケールアウト
3.ロールアウト&ロールバック
4.永続ストレージ利用
5.自己修復(可用性)
6.クラスタの分割利用
7.監視&ログ分析

      :
 (中略) : © IBM Corporation 27 Kubernetesを 広めて勢力図を 変えるぞ 参道して シェアを取りに 行くぞ! 複合環境でも 便利♪ 豊富な資金力で 独走を維持するぞ ロックインから 解放だ! チャンス♪ K8sでクラウド・レースの展開に 変化があるかもしれない まとめ •Kubernetesはコンテナの運用基盤 •オンプレ&クラウドで共通のオペレーションで運用できる •必要なインフラ機能が提供され、高効率な運用を実現 •主要クラウドベンダー、ソフトウェア企業が賛同 Kubernetes ハンズオンへ

日本語含めて正しくテキストが抽出できました。

 

むかーしから存在している技術なのですが、イメージマップという便利な機能があります(最近あまり使われなくなったという印象もあります)。これは HTML ページ内の画像内にクリッカブルな領域を複数定義し、いずれかの領域がクリックされたら何らかの処理を行う、というものです。クリックされる領域によって処理内容(このページにジャンプするとか、この JavaScript 関数を実行するとか、・・)を変えることができる、というものです。

例えばこんな感じで実現します。いらすとやさんの『ホワイト企業~ブラック企業のイラスト』を例に紹介します:



このイラスト画像は横650ピクセル、縦137ピクセルです。その中にホワイト企業~ブラック企業が5段階のイラストで表示されています。

画像の左上の座標を ( 0, 0 ) とすると、このうち一番左の企業は、そのビルの矩形部分は左上が ( 37, 13 ) から右下 ( 100, 127 ) という矩形でできています。いま、このビルの矩形部分がクリックされたら "white" というメッセージを表示するようにしてみます:
2019100701


同様にして、5つのビルそれぞれにクリッカブルな矩形部分を定義し、それぞれがクリックされた時に以下のようなメッセージが表示されるようにしてみます:
ビル番号(左から何番目のビル)矩形範囲クリックされた時のメッセージ
1 ( 37, 13 ) - ( 100, 127 ) white
2 ( 165, 13 ) - ( 227, 127 ) lightgray
3 ( 291, 13 ) - ( 355, 127 ) gray
4 ( 481, 13 ) - ( 483, 127 ) darkgray
5 ( 546, 13 ) - ( 610, 127 ) black


これをイメージマップで実現すると、 HTML の該当箇所は以下のようになります:
<body>
  <div class="container">
    <img src="https://1.bp.blogspot.com/-BmJohMucVBI/XYhOV_VMQmI/AAAAAAABVHA/sZF8lMjPedUWSkxBwUGZXmVri2OFKEZ4gCNcBGAsYHQ/s650/company_white_black_kigyou_5dankai.png" border="0" usemap="#image_map"/><br/>
    <map name="image_map">
      <area shape="rect" coords="37,13,100,127" href="javascript:alert( 'white' )"/>
      <area shape="rect" coords="165,13,227,127" href="javascript:alert( 'rightgray' )"/>
      <area shape="rect" coords="291,13,355,127" href="javascript:alert( 'gray' )"/>
      <area shape="rect" coords="418,13,483,127" href="javascript:alert( 'darkgray' )"/>
      <area shape="rect" coords="546,13,610,127" href="javascript:alert( 'black' )"/>
    </map>
  </div>
</body>

<img> タグの usemap 属性でイメージマップの名称を定義します。そして <map> タグを使って、対象(name 属性で指定)の <img> のクリッカブル領域と、クリックされた時のハンドラを href 属性に定義します。上記例ではハンドラを javascript 関数の実行にしていますが、普通に URL を記述すればリンクを作成することもできます。

これで各ビルをクリックした時に、(左から順に)"white", "lightgray", "gray", "darkgray", "black" というメッセージが表示されるようになります。元は1枚の画像でしたが、クリッカブルな領域を5箇所定義することができ、それぞれのクリックハンドラを個別に設定することもできました:

(左から2番めのビルをクリックした時の様子)
2019100703


ここまでが普通のイメージマップです。このイメージマップ、非常に便利な反面で「レスポンシブ対応が難しい」という難点がありました。要はクリッカブルな矩形領域をピクセル絶対値で指定しているため、画像がそのまま表示された場合はいいのですが、レスポンシブ対応などで画面サイズに合わせて拡大縮小されて画面いっぱいに表示された場合、縮尺が変更になってしまい、絶対値で指定していたイメージマップの定義がおかしくなってしまうのでした。

具体的に同じコードをスマホのシミュレーターで確認した結果、下の赤点線部が "lightgray" のクリッカブル矩形部分となっていて、本来の位置からズレていました。これをどうにかしたい、というのが今回のテーマです:
2019100702


色々と調べましたが、答としては jQuery RWD Image Maps という jQuery プラグインを使うことで解決できました:
2019100700



以下に具体的な対応手順を紹介します。

まず HTML 内でレスポンシブのための宣言をしておきます。例えば以下のようなコードが既に含まれているものと仮定します:
  :
<meta name="viewport" content="width=device-width,initial-scale=1"/>
  :


次に jQuery と jQuery RWD Image Maps をロードします。以下の例では CDN からそれぞれをロードしています(jQuery RWD Image Maps の方を後にロードする必要があります):
  :
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery-rwdImageMaps/1.6/jquery.rwdImageMaps.min.js"></script>
  :

CSS で画像サイズの調整を行います。この例ではイメージマップを用いる画像の横幅を 100% に、高さは自動調整するように指定しています:
  :
<style>
img[usemap]{
  max-width: 100%;
  height: auto;
}
</style>
  :

そして最後に JavaScript で該当部分に RWD Image Maps を適用します:
  :
<script>
$(function(){
  $('img[usemap]').rwdImageMaps();
});
</script>
  :

これでスマホでおなじページにアクセスした時でもイメージマップの矩形部分がズレることなく利用できるようになりました:
2019100704




このページのトップヘ