patorashのブログ

方向性はまだない

Docker上でWebpackerがファイル変更を検知できない件に対応した

担当しているRailsプロジェクトのフロントエンド環境をSprocketsからWebpackerに移行しようとしていて、ここ最近辛い毎日です。

とりあえず、scssの移行は終わったので、今はCoffeeScriptのコードをできるだけそのままでWebpackerに乗っけようとしているのですが、エントリーポイントのファイルを修正しても全然再コンパイルが走らなくて毎回webpack-dev-serverを再起動するというめちゃくちゃ辛いことをしていました。時間かかりすぎる…。

いい加減、原因を探ろうと調査しだしたのですが、私の開発環境は全てDockerに移行しています。

patorash.hatenablog.com

webpack-dev-severもDockerで起動するようにしてあったのでRailsのコンテナとwebpack-dev-serverのコンテナがうまく連携できてないのが原因なのかなと思い、ググった記事を参考にコンテナを作り直したりとか、色々していました。 ローカル環境でwebpack-dev-serverを起動したら、ファイル変更の検知が効いたのに、Dockerコンテナ経由だと効かなかったので、ファイルの変更の検知をポーリングできないものか…と思ったらwebpackにそういう設定ありました。

webpack.js.org

じゃあそれをwebpackerでどうやって設定すればいいか?というところですが、config/webpacker.ymlで設定できます。

webpacker.ymlwatch_optionsにpollを追加します!pollはミリ秒を設定するので1秒毎にポーリングするよう、1000を設定してみました。

development:
  # 略
  dev_server:
    # 略
    watch_options:
      ignored: '**/node_modules/**'
      aggregate_timeout: 200
      poll: 1000

これで、変更があった分のファイルのみ再コンパイルが実行されるようになりました。速い!!🚀

まだ全然Sprocketsからの移行は終わってないのですが、やる気がみなぎってきました。

7年開発しているプロジェクトをRails6にアップグレードした

Railsのアップグレード作業は検証含めて非常に面倒なもので、結構遅れがちなのですが、どこかで気合い入れてやらなければなーと思い、エイヤッとやってやりました。Rails3からやってるプロジェクトなので負債もまだまだ多いですが、とりあえずメジャーバージョンアップさえしておけば、他の技術も徐々に対応できるはず…。今回のアップグレードは5.2系から6.0系へのアップグレードです。

今回のアップグレードでやったこと

トレンドに追従できていないのは全然わかっていますが、頑張って追いかけるためにやれることはやっていきます😀

  • Sprocketsを3から4に変更した
  • sass-railsを6に変更した
  • therubyracerをmini_racerに変更した
  • Rails6をインストールしてbin/rails app:updateで差分を反映した
  • belongs_toにoptional: trueを付けていった
  • app/views/以下にある*.js.coffeeのファイルを*.js.erbに変更してCoffeeScriptからJavaScriptに修正した
  • Rails6からのローダーであるzeitwerkに対応するため、bin/rails zeitwerk:checkを行い、クラス名を修正したり等を行なった
  • ActionDispatch::HostAuthorizationを有効にするためにconfig.hostsに設定を行なった
  • credentials機能をバックポートしていたので、それを削除した

個別に解説するのもあれなので、ざっくりと書いていきます。

Sprocketsを3から4に変更した

まず雑にRails6系にしてbundle update railsをしたら、色々と怒られまして、その中に入っていたと思います。これはSprockets3系に依存していたgemがあったためでした。たしか、css_spritterだったかなと思います。これはIE9が1ファイルに付き4096までしかセレクタ定義を認識できないため、それ以上の定義を使おうと思ったらファイル分割するしかない、というやつですね。

これは昔、私がqiitaに記事を書いてます。

qiita.com

もうIE9のサポートは打ち切ったので、これを削除し、Sprocketsのバージョンを4系にしました。 この辺りでsass-railsを6にするのもなんか時間かかった気がしますが、何が原因だったか忘れました。

therubyracerをmini_racerにした

mini_racerの存在に気づいたのがつい最近というレベルで知らなかったのですが、以下の記事は3年前の記事です。これといってtherubyracerに依存しているようなものはないだろう、と思って変更しました。テストも通っているので大丈夫でしょう。

note.com

belongs_toにoptional: trueを付けていった

これはもう以前のバージョンの時点でやっておくべきだったことですが、放置していたので対応しました。belongs_toをつけると関連先がデフォルトで必須になる件です。config.load_modules 6.0に設定して、頑張って直しました。

views以下のjs.coffeeをjs.erbに修正した

これは、assets以下のほうはjs.coffeeで動くのですが、views以下のほうがjs.coffeeを認識しなくなっていました。Ajaxでアクセスされた際に返すViewが以前は自動的にjs.coffeeになっていたのだけれど、ファイルの存在を認識してくれないからhtml側のViewを返してしまってエラーになっていました。

このままで動く方法を探そうとしたのですが、いまいちわからず…。Railsがcoffeeをサポートしなくなったからなんでしょうか?直し方がわからなかったので、拡張子のjs.coffeeをjs.erbに変換して、ファイルの内容もCoffeeScriptからJavaScriptに直したら、普通にAjaxで認識してくれるようになりました。

Ajaxで返す際のCoffeeScriptでは、大したことをしていなかったのでJSへの修正も簡単、かつファイル数も少なかったため、手動で直して終わり、としました。

zeitwerk対応

bin/rails zeitwerk:checkを行い、クラス名の修正等を行ないました。

zeitwerkはフォルダ構造とファイル名からnamespaceやクラス名を推測してロードを試みるため、多少の修正が必要でしたが、先ほどのコマンドで「こう直してほしい」と言われるので、大したことはありませんでした。一斉置換で対応可能でした。

ActionDispatch::HostAuthorization対応

これは、DNSバインディング攻撃から保護するために追加されたミドルウェアの設定です。

config/application.rbやconfig/environments/production.rbなどで設定しておくと、設定されたドメイン以外でのアクセスを受けたらエラーになってくれます。

いろんなサブドメインで検証することもあるので、config/application.rbで以下のようにしました。(example.comはプロダクトのドメインに置き換えてください) .example.comとすることで、サブドメインワイルドカードにすることができます。

config.hosts.push 'example.com', '.example.com'

設定はこれでいいのですが、インテグレーションテストを実行すると、この設定が仇となってエラーになります。

そこで、config/environments/test.rbの最後で、クリアしておきます。

config.hosts.clear

これで、テストも無事実行されるようになりました。

credentials機能をバックポートしていたので、それを削除した

これは以前に記事にした通り、Rails6系のcredentialsの機能を5.2にバックポートしていたのですが、正式に6系にしたため、不要になりましたので、削除しました。

patorash.hatenablog.com

削除した後に動作検証をして、問題ないことを確認しました。

これからやりたいこと

あくまでも私見ですが…

  • CoffeeScriptをやめる
  • Sprockets4からWebpackerに移行する
  • bootstrap3から4に移行する
  • knockout.jsからstimulusに移行する
  • turbolinksを導入する

周回遅れ感がありますが、保守をしつつ、機能追加をしつつ、アップグレードしていくのは大変なのです😢若者がプロジェクトに入ってくれることになっているのですが、若者に今更CoffeeScriptとかやってもらうのは厳しいと思うので、本格的にアサインする前にこれらの一部でも倒しておきたい所存…。やっていくしかない。

c3.jsのスタックバーチャートの表示順を変更する

c3.jsでスタックバーチャートを書いた際に、スタックバーの順番を変更できないか?という要望を受けたので調査しました。凡例の順番とツールチップで表示される順番がリンクしていないので、見辛いということでした。

公式ドキュメントによると、data.orderに指定できるようでしたが、

  • 値の昇順(asc)
  • 値の降順(desc)
  • 順番を決める関数を定義する(function)
  • 指定しない(null)

しかなさそうでした。

C3.js | D3-based reusable chart library

f:id:patorash:20200609132535p:plain
data.orderに何も指定しないと順番が適当に…

試しに、data.orderにスタックバーに表示するjsonのkeyを配列で渡したところ、その順番に表示することができました!

コード

まず、コードペンで書いたコードを載せておきます。

See the Pen c3-stacked-bar-chart-order by patorash (@patorash) on CodePen.

解説

data.orderに、data.keys.valueで指定した配列とは逆順に指定しています。reverse関数を使うのが簡単でしょう。

data: {
  json: json,
  type: 'bar',
  keys: {
    value: ['data1', 'data2', 'data3', 'data4']
  },
  groups: [['data1', 'data2', 'data3', 'data4']],
  labels: {
    format: d3.format('01,d')
  },
  order: ['data1', 'data2', 'data3', 'data4'].reverse()
},

こうすると、スタックバーの順番は制御できたのですが、今度はツールチップで表示される値の順番が逆になってしまいました…😢

f:id:patorash:20200609133233p:plain
スタックの順番は制御できたものの、凡例の順番が逆に…

そのため、tooltip.orderを指定します。

tooltip: {
  order: ['data1', 'data2', 'data3', 'data4']
},

これで、期待した順番で表示されるようになりました。

f:id:patorash:20200609133902p:plain
凡例の順番に並ぶようになった

c3.jsでスタックバーチャートをパーセント表示にする

またまたc3.jsネタです。

c3.jsでスタックバーをするのは簡単で、そのスタックバーの軸を100%にするように表示するのも簡単です。dataのところでstack: { normalize: true }とすればできます。

しかし、それだとチャートは%表示になるのですが、ツールチップに表示されるのは実数です😥

f:id:patorash:20200608024545p:plain
実数ではなく、%が知りたい

そこで、今回はこれを割合表示(%表示)にしてみます。

また、前回のと合わせて、ラベル中央に%表記してみます。前回の記事はこちら。

patorash.hatenablog.com

コード

まず、CodePenで書いたコードを載せておきます。プレビューを見てもらえばわかるかと思いますが、%表記になっています。

See the Pen c3-stacked-bar-chart-label-middle-percentage by patorash (@patorash) on CodePen.

解説

c3.jsでは、パイチャート等は%表示する方法があるらしいのですが、なぜかスタックバーチャートにはありません。なので、自力で実装していきます。

labelsのformatの指定を関数にして上書きする

labels:formatは通常はフォーマットを指定するだけですが、関数を取ることもできます。そこで、関数を使って割合を算出して出力させます。

labels: {
  format: (v, id, i, j) => {
    const sum = Object.values(json[i]).reduce((sum, value) => { 
      return sum + value;
    }, 0);
    return d3.format('.1%')(v/sum);
  }
}

関数の引数のvは実数、idはx軸のラベル(item0やitem1等だったはず)、iはループの何番目(item0ならば0, item1なら1等)か、jはiのループ内の更に何番目か(item0のdata1なら0, data2なら1等)を表します。

今回は単純に割合を知りたいので、item0, item1, ..., item4のそれぞれの合計値を取得し、それで実数を割ることで割合を出したいので、変数jsonからループ番目毎に値だけの配列を取得し、reduceで合計値を求めるようにしました。

最後に、d3.format('.1%')(v/sum)とすることで、小数点一桁までを出すようにしています。

これで、ラベルのほうを%表示にすることができました。ツールチップはまだなので、ツールチップも設定していきます。

tooltipのformat指定を関数にして上書きする

ツールチップのほうはめちゃくちゃ簡単です。なぜならば、引数に既にratioがあるからです。先ほどと同じように、d3.format('.1%')(raito)とするだけです。labelsのほうにも最初からratioがあればいいのに!!😅

tooltip: {
  format: {
    value: (value, ratio) => d3.format('.1%')(ratio)
  }
},

なにはともあれ、これでスタックバーチャートの表記が%表記になりました💪

f:id:patorash:20200608032356p:plain
%表記になったスタックバーチャート

検索用の文字を最後に残しておきます。

  • d3.js
  • stacked bar chart
  • percentage

c3.jsでスタックバーチャートのラベルを縦中央揃えにする

c3.jsでスタックバーチャートを描画するのは、比較的簡単です。dataのところでgroupsに含めてしまえばいいです。 しかし、値のラベルがバーの上に表示されてしまいます。これでもいいケースは多いと思いますが、ラベルの色がバーの色になるため、合わない色の上になる可能性があり、見づらくなりそうです。

f:id:patorash:20200608013519p:plain
スタックバーチャートのラベルがバーの上に表示される

そこで、今回はバーの中央に表示してみようと思います。

コード

まず、CodePenで書いたコードを載せておきます。プレビューを見てもらえばわかるかと思いますが、縦中央ぞろえになっています。

See the Pen c3-stacked-bar-chart-label-middle by patorash (@patorash) on CodePen.

解説

c3.jsには、バーの中央に値を表示するような設定はありません。そのため、なにかしらの方法で描画位置を変えなければなりません。

onrenderedを使う

onrenderedイベントで、レンダリング後に操作を行うことができます。

d3のselectAllメソッドを使って、バーのNodeを取得し、それぞれの高さを2で割った値を取得します。 そして、ラベルのノードを取得し、その高さに移動するようにtransformを使って移動させます。

onrendered: () => {
  centers = d3.selectAll("#chart .c3-chart-bars .c3-bar").nodes().map((el) => el.getBBox().height/2);
  d3.selectAll("#chart .c3-chart-texts text.c3-text").nodes().forEach((el, i) => {
        d3.select(el)
          .attr('transform', `translate(0, ${centers[i]})`)      
  });
}

cssで微調整する

これだけだと、ラベルの色とバーの色が同色のため、何も見えなくなります。そこで、cssで文字の色を強制的に白にします。 また、デフォルトのbaselineだと微妙にずれるので、dominant-baselinemathmaticalに指定することで本当の中央になるようにしました。

.c3 .c3-chart-texts text.c3-text {
  font-size: 14px;
  fill: white !important;
  dominant-baseline: mathematical;
}

dominant-baselineの値の説明は、以下の記事を参考にしました。

qiita.com

これで、ラベルがスタックバーの上に、縦中央揃えで表示されるようになりました😀

f:id:patorash:20200608021859p:plain
縦中央揃えにラベルが表示された状態

d3.js, c3.jsで描いたチャートを印刷する

d3.jsを皆さまはご存知でしょうか?data-driven-documentの頭文字をとって、d3.js。JavaScriptsvgでチャートを書くためのライブラリです。

d3js.org

そして、d3.jsをベースにチャートを生成しやすくしたライブラリがc3.js。

c3js.org

c3.jsで描いたチャートを印刷したかったのですが、d3.js・c3.js共に印刷とかファイルへの保存とかには対応していないみたいだったので、自分でなんとかしてみました。 元々は、highchartsというライブラリを使っていたのですが、有償ということもあって、別にものに変えたかったというのがありました。こちらは、印刷とかpngやpdfにして保存という機能が元々あって便利です。

www.highcharts.com

方針

チャートだけ印刷したかったので、以下の手順で実現することにしました。

  1. ポップアップウィンドウで別ウィンドウを開く。
  2. そこに親ウィンドウのsvgタグの内容をマルッとコピーして突っ込む。
  3. 印刷を起動する。
  4. 印刷が終わったらポップアップウィンドウを自動で閉じる。

とりあえずは、これでやりたいことは実現できました。

ハマったこと

IE11と旧Edgeの対応具合が悪い

d3.jsでチャートを書くときに、IE11でエラーが起きていました。d3.jsのバージョン5だと、fetchやpromiseを使っているので、それが存在しないということでエラーになっていたので、polyfillを入れました。 また、svgタグの内容をinnerHTMLで取得していたのですが、IE11だとsvgタグ内の内容をinnerHTMLで取得できないという仕様のようでした😨 それもまた、Googleが作っているinnersvg-polyfillというライブラリを入れることで解決しました。

最初はjQueryのouterHTMLを使っていましたが、jQueryに依存した状態になるのも微妙なので、XMLSerializerでNodeを文字列に変換するのがよいという記事を見かけたのでそうすることにしました。

c3.jsのcssを読み込まなければならない

c3.jsはチャートの表示の整形にcssを使っているので、それをポップアップウィンドウ側でも読み込まなければなりません。

IE11がafterprintイベントを拾わない

イベントリスナを定義して、afterprintでポップアップウィンドウを閉じていたのですが、閉じてくれないケースがありました。 かなり悩んだのですが、setTimeoutで閉じるという技を見かけたので、これを採用しました。

上記の問題を解決したコード

CoffeeScriptですが、以下のような感じになりました。

# self.svgには、d3.jsのSelectionオブジェクトが入っている
printWindow = window.open('', 'PrintChart', "width=#{self.svg.attr('width')}, height=#{self.svg.attr('height')}")
printWindow.document.writeln('<head><link rel="stylesheet" href="/css/c3.css" media="all" /></head>')
printWindow.document.writeln("<body class='c3'>#{new XMLSerializer().serializeToString(self.svg.node())}</body>")
printWindow.document.close()
printWindow.print()
setTimeout ->
  printWindow.close()
, 1000

動かしてみる

実際に開いた印刷画面はこちら。(Chromeにて)

f:id:patorash:20200603181202p:plain
c3.jsのチャートをポップアップウィンドウで表示して印刷

とりあえず印刷できるところまではいけたのでOK!!👍

他にもボチボチc3.jsのノウハウが溜まってきたので書いていこうと思います。

LenovoのPCのバッテリー消費が激しいのは高速スタートアップが原因だった

現在は個人PCはLenovo L380 Yogaを使っているのですが、電源を切っているにも関わらず翌日起動したらバッテリーの残量が30%近く減っていて物凄く驚きました。しかし普通に使う分には結構バッテリーも持つので、バッテリーの不具合ではないだろうとは思っていたのだけれど、それでも電源を切って数日でバッテリーが激減しているのはずっとストレスでした😥

まぁそんなにACアダプタの使えない場所で作業することもなかったので、1年以上我慢していたのですが、原因が分からずにずっと悩んでいたので、久々にググってみたところ、高速スタートアップが原因じゃないか?というのを見かけました。

高速スタートアップは、次回のパソコンの起動を速くするために、CPU・メモリの状態を保存しておく仕組みらしいです(それってシャットダウンか…?🤔) デフォルトで高速スタートアップは有効になっているらしいですが、正直デフォルトは無効にしろよ…と思います。バッテリーの初期不良かと思ったじゃないか…😡

無効にする方法を調べたところ、Lenovoのサイトでも紹介されていました。

support.lenovo.com

無効にしたところ、確かに起動は少々遅くなりましたが、バッテリーは数%程度しか減っていませんでした👍これで安心して持ち運ぶことができます!!