yuw27b’s blog

技術メモと雑記

SVGを描くJavaScriptライブラリ

この記事は、SVG Advent Calendar 2015 - Adventarの15日目の記事です。

チャートやグラフ用のJavaScriptライブラリはどれがいいんだろう?

2015年は「Ajaxで取ってきたデータを、SVGのチャートやグラフにして表示する」という作業をわりとたくさんやりました。
実装方法もいろいろやっていて、

などなど・・・。
小規模サイトだったり、公開範囲の限定されたサイトだったり、という事情に甘えて、色々なものに手を出した感じですね(笑)。

ピュアJavaScriptは、まさに13日のこちらの記事(気づいて築く: パイチャートのSVG仕立て 〜アニメーションを添えて〜)みたいなことを四苦八苦しながらやってました。

どれがベストかは、ケースバイケース、だとは思いますが、
「普段使ってるようなWebサービスはどうやって実装されてるんだろう?」と思って、いくつかソースコードを見てみた、というのがこの記事です。

GitHub

f:id:yuw27b:20151215232116j:plain
※各リポジトリの「Graphs」タブのところ

Google Analytics

d3.jsも使っているというのがちょっと意外でした。

StatCounter

このライブラリは有料のよう。

SimilarWeb

Lazy Line Painterというのは、線のアニメーション用みたいですね。

Qiita

f:id:yuw27b:20151215232124j:plain
※ユーザーの「投稿タイプ」のパイチャートのところ


2016/1/26追記:
UIがリニューアルされましたね!
f:id:yuw27b:20160126211924j:plain
新しいチャートは、d3.jsのラッパーライブラリc3.jsを使用しているようです。
ちょうどIE8のサポートも切れたので、Raphaëlである必要がなくなった、ということかもしれません。




RaphaëlはIE8でも表示できるので重宝してました。
最近はIE8対応不要なケースが増えて、私はあまり使わなくなってしまいましたが、
Raphaëlの作者さんがSnap.svgを作っているので、今度はそちらでお世話になろうと思います。

d3.jsは一番よく見かけるし、サンプルも多くて便利だな、と思います。
反面、サンプルそのままで事足りてしまうケースも多くて、そうすると「いかにもd3.js」な見た目になるので、
ゼロベースで実装できるように勉強したい・・・というのを来年の目標にしようと思います。


また見つけたら追記します!
普段、いろいろなところでSVGのチャートを見ている気がしていましたが、いざ探そうとすると思い出せないものですね><




12/16追記:
SVG Advent Calendar、翌日16日は空きだった・・・と思いきや、
カレンダーの作成者でもあるrikuoさんがSVGでFizzBuzz(XSLT版) - 週刊SVGという記事をアップされていました。XSLTでプログラミングしていてすごい・・・!
もしかして簡単なチャートならJavaScript使わなくても書けちゃったりするんでしょうか。画像でもありテキストでもあるSVGって本当におもしろいですね。

writing-modeがFirefoxでも使えるようになっていた

CSSで縦書きテキスト

CSSのみで縦書きテキストを実現するには、writing-modeプロパティを使いますが、Firefoxだけが長いあいだ未対応でした。
Can I use... Support tables for HTML5, CSS3, etc

2015年11月現在、この仕様は勧告候補の段階なので、ブラウザによってベンダープレフィックスが必要だったり、指定する値の記述方法がばらばらだったりもします。
そして、未対応だったFirefoxは、バージョン41からは標準で対応するようになったとのこと(ベンダープレフィックスもなし!)。
Firefox 41 for developers - Mozilla | MDN

ほぼ全てのブラウザが対応したことに

IEは(値の記述方法は独自仕様ですが)6から使えますし、ChromeSafariはベンダープレフィックス付きで対応しているので、Firefoxが対応したことでほぼ足並みが揃ったことになります。

現時点で各ブラウザに対応させるには、こんな感じになります。

writing-mode: tb-rl; /*IE*/
-o-writing-mode: vertical-rl; /*Opera*/
-webkit-writing-mode: vertical-rl; /*Chrome, Safari*/
writing-mode: vertical-rl; /*Firefox, Edge*/

※縦書きで、行が右から左へ進む場合の指定方法です

まだフォールバックが必要?

writing-modeに対応したFirefox41のリリースは2015年9月22日と(これを書いている時点からすると)かなり最近なので、基本的に自動アップデートがかかるとはいえ、まだ40以前のバージョンのユーザーもいるかも?という不安もあります。
さらに、FirefoxにはESRという法人向けの延長サポート版が存在していて、こちらはまだバージョン38です。これが2016年5月末までのサポート予定とのことなので、やはりそれまでは何かしらフォールバックを用意したほうが良さそうです。

フォールバックも書いてみた

縦書きのテキストが横書きになってしまった場合、コンテンツのレイアウトにかなり影響が出るので、それを回避するべく以下のような方法をとりました。

  • writing-modeが使えない場合、横書きのテキストを90度回転させる
  • 回転はCSSのtransformプロパティで行う(これはFirefox16から対応している)
  • ブラウザがFirefox40以前かどうか、という点はJavaScriptで判別する

日本語の場合はテキストが横倒しになるので、writing-modeが適用された状態と全く同じにはなりませんが、フォールバックということで、そこまでの対応としました。

Chrome(writing-modeに対応)
f:id:yuw27b:20151110231711j:plain

Firefox38(writing-modeに非対応)
f:id:yuw27b:20151110231718j:plain

HTML

<div class="box">
    <p class="text">日本語のテキスト<br />改行</p>
</div>


CSS

.box {
	margin: 20px;
	border: solid 1px #E00;
        line-height: 0;
}

.text {
	display: inline-block;
	margin: 0;
	font-size: 18px;
	line-height: 1.5;
	color: #000;
	background: #CCC;
	writing-mode: tb-rl;
	-o-writing-mode: vertical-rl;
	-webkit-writing-mode: vertical-rl;
	writing-mode: vertical-rl;
}

.text-rotate { /*writing-mode fallback*/
	transform-origin: 0 0;
	transform: rotate(90deg) translateY(-100%);
}


JavaScript(※DOMを読み込み終わってから実行)

  var ua = window.navigator.userAgent;
  if (!('indexOf' in Array.prototype) || ua.indexOf('Firefox') === -1) {
    return;
  }
  var browser_str = ua.match(/Firefox\/.*$/);
  var version = parseFloat(browser_str[0].replace(/^Firefox\//, ''), 10);
  if (version < 41) {
    document.querySelector('.text').classList.add('text-rotate');
  }

包含ブロック(.box)に赤い枠線を、テキストの要素(.text)にはグレーの背景をつけました。
テキストの要素(.text)に「display: inline-block」を指定しているのは、rotateを使用した(フォールバックの)場合に、.textの高さが.boxの横幅100%になってしまうからです。

また、画像で分かるように、rotateを使用した場合は包含ブロックの高さは伸びません。
包含ブロックがテキストの要素(.text)よりも縦に長い場合は問題ありませんが、必要に応じてmin-heightを指定するなどすると良さそうです。

CADDYで手軽にHTTP/2サーバを立てる

CADDYとは

公式サイト
Caddy - The HTTP/2 Web Server with Fully Managed TLS

GitHub
GitHub - mholt/caddy: Fast, cross-platform HTTP/2 web server with automatic HTTPS

公式サイトから抜粋:

  • Windows, Mac, Linuxと、Androidで動く
  • Apacheやnginxのような細かな設定はできないかわりに、設定に関する専門知識は不要
  • 静的なファイルのサーブを主目的にしていて、環境の移行も簡単

ローカルでちょっとテストするのに使えるかなー、と思って興味を持ったのですが、
もう少し本格的なサーバなのかも?という印象。
HTTP/2で動かすのは本当に簡単で、PHPもHTTP/2でサービスさせることができました。


以下、Macでの設定〜起動までの手順です。
※他のOSでは試していませんが、サーバ自体が実行ファイル形式になっているので、基本的には同じような方法で動かせるのではないかと思います。

ダウンロードとインストール

Download Caddy
本体の「Caddy Core」をダウンロードします。

f:id:yuw27b:20151108212649p:plain

ダウンロードしたフォルダ直下にある「caddy」が実行ファイルです。
好きな場所に置いて、シェルからコマンドを実行できるようにパスを通しておきます。

ターミナルから、サーバのドキュメントルートにしたいディレクトリに移動して、

$ caddy

でサーバが起動します。
デフォルトでは「2015」番ポートを使うようになっているので(来年になったら変わるとか?)、ブラウザで「http://localhost:2015/」にアクセスしてみます。
「index.html」があればその内容が表示されるはずです。なくても「404 Not Found」が出ていればサーバは動いています。
f:id:yuw27b:20151108212725p:plain
↑デフォルトの404画面はすごくシンプル。

HTTP/2でサービスさせるために、SSLの設定をする

HTTP/2に対応させるにはSSLの設定をして、HTTPSを使用する必要があります。

2017年2月追記:
CADDYに、自動的に自己証明書を生成するオプションが追加されましたので、テスト用途であれば以下の証明書の準備は不要になりました。便利です!
公式ページ:tls - Caddy Directives (追記ここまで)

今回は、ローカルのテストサーバということでセキュリティは考えずに、opensslで自己証明書を用意します。

※証明書について詳しいことは省きますが、以下が参考になりました:
オレオレ証明書をopensslで作る(詳細版) - ろば電子が詰まっている
OpenSSLコマンドによる公開鍵暗号、電子署名の方法 - Qiita

鍵ファイルを置きたいディレクトリに移動してopensslのコマンドを実行します。

$ openssl genrsa -aes128 -out server.key 2048
$ openssl req -new -key server.key -sha256 -out server.csr
$ openssl x509 -in server.csr -days 365 -req -signkey server.key -sha256 -out server.crt

※openssl reqのところで、いろいろ入力項目が出てきますが、パスワード以外はすべて空欄のままで問題ありませんでした。

これで鍵セットは完成ですが、暗号化されたままだとうまく動かなかったので、キーを復号したものも作りました。

$ openssl rsa -in server.key -out server.noencrypt.key

これでSSLの準備は完了です。
(※復号化しないと動かなかったのは、何かミスっているような気もするのですが、ローカルサーバなので追求していません。)

とりあえず、サーバもSSL keyもコンテンツもこんな感じで一ヶ所にまとめました。

f:id:yuw27b:20151108213106p:plain

もちろんどこに何を置いてもいいのですが、まとめておいたほうがパスの指定などがやりやすいかな、という印象です。

caddyfileを用意して細かな設定をする

CADDYでは「caddyfile」というテキストファイルに様々な設定を記述することができます。
設定可能な項目は公式サイトの以下のページにまとまっています。かなりたくさんあります。
The Caddyfile - Caddy

シンプルなHTTP/2(HTTPS)のローカルサーバの場合:

localhost:2015 {
  tls ./sslkey/server.crt ./sslkey/server.noencrypt.key
  root ./public/
}

毎回ドキュメントルートに移動してサーバを起動するのも面倒なので、「root」をcaddyfileに記述しました。
※caddyfile内に記述するパスは、カレントディレクトリからのパスです。上の例では相対パスになっていますが、もちろん絶対パスでも大丈夫です。

2017年2月追記:
CADDYに自己証明書を自動生成させる場合は、

  tls self_signed

と記述します。(追記ここまで)

caddyfileを保存したら、ファイルの内容をcaddyに渡して実行します:

$ caddy -conf="/path/to/Caddyfile"

もしくは、

$ cat Caddyfile | caddy

ブラウザから、今度は「https://localhost:2015」にアクセスしてみます。(証明書の警告が出ると思いますが、無視して接続を選択します。)
f:id:yuw27b:20151108213220p:plain

ブラウザが対応していれば、HTTP/2での接続になっているはずです。
ChromeのDevToolsで見てみると、「Protocol」のところが「h2」となっているのが確認できます。
f:id:yuw27b:20151108213238p:plain


HTTP/1.1 vs HTTP/2

簡単なテストとして、画像を10枚配置したHTMLを作ってアクセスしてみました。
上がHTTP/1.1で下がHTTP/2です。HTTP/2では10枚が並列に読み込まれています。

f:id:yuw27b:20151108213259p:plain
(ローカルサーバにしては時間がかかっていますが、差が分かりにくいのでブラウザのほうでわざと速度を落としています。)

FastCGIを使う

CADDYでは、そのままでは静的なファイルのサーブしかできませんが、リクエストをFastCGIにプロキシすることで、PHPの実行が可能になります。
fastcgi - Caddy Directives

まず、PHPFastCGIマネージャであるphp-fpmを起動しておきます。
php-fpmが9000番ポートでサービスしている場合、caddyfileを以下のように記述します。

localhost:2015 {
  tls ./sslkey/server.crt ./sslkey/server.noencrypt.key
  root ./public/
  fastcgi /api 127.0.0.1:9000
}

この設定では、「/api」以下のリクエストを、FastCGIにプロキシします。

phpinfo()を呼び出すと以下のようになりました。
f:id:yuw27b:20151108213315p:plain


おわりに

テストサーバとしてやりたかったことはこれで全部できたので、ひとまずここまで。

処理速度やどのくらいのトラフィックをさばけるのか、というパフォーマンス面と、まだいろいろなことができそう、という機能面を、もう少し調査できたらまた続編を書いてみたいと思います。

language-sparqlというAtomのPackageを作った

atom.io

AtomでSPARQL(RDFデータを検索する言語です)のシンタックスハイライトがなかったので、作りました。
「作った」と言っても、文法の定義はSublime Text用のパッケージから持ってきたので、大したことはしていません。


AtomのPackageを公開する手順は、オフィシャルのドキュメントでひととおり把握できました。
あとは、他の言語用の(シンタックスハイライトの)Packageのソースコードを見て参考にしたりもしました。


公開したのは1ヶ月前ですが、だいたい1日1回くらいのペースでダウンロードされているようです。
なんですが、ちょっとミスって最初のバージョンが「0.2.0」になっています・・・恥ずかしい・・・。
package.jsonに自分で「"version": "0.1.0"」って書いたのが原因です。
apm public minor」をしたときに、Atomのほうでバージョン上げてくれるので「0.0.0」のままで良かったんですね。


SPARQL自体は、今のところはお手伝いでちょっと書いたくらいです。
オープンデータを使ったwebアプリとか、何かアイデアが浮かんだらやってみたいです。

JavaScriptのPromiseとarray.reduceを合わせて使う

順番に非同期の処理をしたい要素たちを配列に入れて、各要素に対してPromiseを返す関数を実行する。

例)['a', 'b', 'c']という配列があったときに、それぞれの文字を2秒おきにコンソールに表示したい

//これだと2秒後にほぼ同時に「a」「b」「c」がコンソールに表示されるのでNG
function wait2sec (item) { //2秒後に引数itemをコンソールに表示するfunction
  return new Promise( function(resolve, reject) {
    window.setTimeout(function() {
      console.log(item);
      resolve();
    }, 2000);
  });
}

['a', 'b', 'c'].forEach(function (item) {
  wait2sec(item);
});
//2秒おきにconsole.log()される!
function wait2sec (item) { //このfunctionはさっきと同じ
  return new Promise(function (resolve, reject) {
    window.setTimeout(function () {
      console.log(item);
      resolve();
    }, 2000);
  });
}

['a', 'b', 'c'].reduce(function (prevValue, currentValue) {
  return prevValue.then(function () { ////直前の要素の処理(2秒かかる)が終わってから次の要素をwait2sec()に渡す
    return wait2sec(currentValue);
  });
}, Promise.resolve());


たまにIE8から対応とかあるのでjQueryだけでどうにかする(だいぶ無理矢理感が・・・)

function wait2sec (list, i) {
  var dfd = new $.Deferred();
  window.setTimeout(function () {
    console.log(list[i]);
    dfd.resolve(i + 1);
  }, 2000);
  return dfd.promise();
}

var list = ['a', 'b', 'c'];
var i = 0;
var prevValue = new $.Deferred().resolve(0);
for (i = 0; i < list.length; i += 1) {
  prevValue = prevValue.then( function (next_i) {
    return wait2sec(list, next_i);
  });
};


参考文献

こちらを参考にコード書きました!
JavaScript Promises: There and back again - HTML5 Rocks


Array.prototype.reduceについて

使ったことなかったですが、便利ですね!

Array.prototype.reduce() - JavaScript | MDN

reduce関数は結構有用っていうお話 - あと味

IE9以降で使えるそう。過去に書いたコードの中にも、これを使えばもう少し簡潔に書けたものがありそうな気する・・・。


使いどころ

Ajaxで、サーバ側APIからの応答を待って処理したいときにこんな感じで書いてます。
今のところ、

  • HTMLコンテンツをパーツごとに取得したいケース
  • ファイルを1つずつアップロードしたいケース

に遭遇しました。


非同期のループはまた使いそうなので、備忘録として。