patorashのブログ

方向性はまだない

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

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

SwitchBotのキャンペーンに当選した

Twitterで行われていたSwitchBotのキャンペーンに申し込んでいたら、なんと当選してSwitchBotを送って頂きました!

www.switchbot.jp

うまく動かないケース

付けてみたんだけれど、ボタンが押しきれない…。

そうしたら、Panasonicのボタンは押せないケースがあるというアドバイスを頂きました。本家のサポートに記事が掲載されていました。

support.switch-bot.com

ようやく動いた!

まんまですが、これで動きました。

レビュー

いい点

寝ながらライトを消せるようになったのはよかったです。布団に入る前に消すと布団に入るまでが真っ暗になるので、微妙に思っていたのでそれが解消されました。

また、次の動作時間を決めることができるので、1時間後に消灯するとか、8時間後に点灯することなどできるようになりました。よく読書しながらライトつけっぱなしで寝落ちしていたので、1時間後に消灯するようにしておいたらうまくいきました。

微妙な点

SwitchBot単体ではGoogleアシスタントで操作できません…。希望は音声操作できたらいいなと思っているのですが、SwitchBotだけだとスマホでの操作しかできず、音声で操作したければSwitchBot Hubが必要になります。 SwitchBot HubはSwitchBot製品をインターネット越しに操作できたり、シーンを定義して一斉に操作したりできるので、買ってもいいかな~と思えるのですが、似たような製品のNature Remoを既に家で使っているため、わざわざ買うのがなぁ~…という気持ちで、なかなか更なるスマート家電化に踏み切れません…。

また、モーター音が結構大きいので、寝落ちしていたときに消灯タイマーが作動したときに作動音で起きてしまいました(起きたと同時に消灯…)

総評

赤外線で操作できるものはNature Remoで操作できているから、使いどころがなかなか難しいですが、換気扇のタイマーによさそうとかいう話もTLで出ていたので、使い方次第ではよさそうです。赤外線も使えないアナログなものをIoTにできるので、使い道は色々あるんだろうなぁ~とは思います。でもそんなに家にないんですよねー…。やはりライトくらい。 Nature Remoをまだ使ってないご家庭であれば、SwitchBot Hubを導入してSwitchBotに統一するとめちゃくちゃ便利になると思います!

gem doorkeeperの承認画面に遷移しない場合があった

gem doorkeeperで連携アプリケーションにログインをさせようとしたところ、認証後に権限の承認画面に遷移せずに普通に認証アプリケーションのログイン後の画面に遷移してしまいました。本来ならば、連携アプリケーションにログインしてほしいのに…。

原因

deviseによって使われるメソッドである、after_sign_in_path_forをオーバーライドしていたからでした。

after_sign_in_path_forメソッドは、ログイン後に遷移させる場所を定義するためのメソッドなのだが、これを上書きしていたため、リダイレクト先が認証アプリケーションのログイン後の画面のみになっていました。

before

def after_sign_in_path_for(resource)
  root_path
end

after

stored_location_forというメソッドがあるみたいだったので、これがあるときはこちらを使う、という設定に変更しました。

def after_sign_in_path_for(resource)
  stored_location_for(resource) || root_path
end

Qiitaにも記事がありました。

qiita.com