89
85

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

最近勉強し始めたので、備忘録も兼ねてSVGやD3.jsなどの全体的且つ基本的なところを浅く広く触れます。

SVGとは

Scalable Vector Graphicsの略。普通のビットマップ画像と比べて、拡大しても荒れたりしません。
その代わりに小さい画像で凄い複雑な構造になっていると、負荷が高くなったりします。
IE9以前だとSVG自体やフィルターなどの機能が使えなかったりするので、どうしても古いブラウザまでサポートしないといけない場合は諦めて普通にビットマップ画像を利用します。

D3.jsとは

SVG含めたHTML上でのビジュアライズやインタラクティブな処理・アニメーションが色々できるjsのライブラリ。
シンプルな記述で凝った表現のものが作れます。インタラクティブなグラフ(プロット)を作ったりなどに使われます。
2019年1月現在、githubのリポジトリにおけるスター数が8万を超えてる凄いやつ(適当)。

基本的なプロット目的であれば、D3.jsのラッパーのライブラリで十分対応ができ、D3.jsよりもシンプルに要件を満たせます。それらのライブラリも人気があります。C3.jsとか、Plotlyとか。

学習コストの面もあるので、自分で色々カスタマイズしたくなったらD3.jsの方に手を出してもいいかもしれません。

なお、本記事ではD3.jsは4系のバージョンと5系のバージョンを想定して書いていきます。3系は詳しくないので割愛します。

インストール

scriptタグなりimportなりは公式の記述をご確認ください。

SVGタグの書き方

svgタグを使い、widthとheightでSVG全体のサイズ(画像サイズ的な扱いになる)を指定します。このサイズ外にはみ出る表示要素の領域に関しては表示されません。
デフォルトでは数値はピクセル単位となります。

    <body>
        <svg width="50" height="50">
            <!-- ここに、線や図形・テキストなどの要素を追加していきます。 -->
        </svg>
    </body>

※以降の例では、D3.jsを使わずに直接SVGを記述する方法を記載していきます。
直接書くケースは少ないかもしれませんが、知っておくとD3.jsで扱う際にもスムーズに扱えます。

SVGで四角を描く

rectタグを使います。
SVG内での座標とサイズをそれぞれx, y, width, heightの属性で指定します。

        <svg width="50" height="50">
            <rect x="10" y="10" width="30" height="30" />
        </svg>

※座標などが分かりやすいように、CSSでSVGの背景色を灰色にて表示しています。黒の部分が今回の四角の描写領域となります。

20190109_2.png

SVGで角丸の四角を描く

こちらもrectタグを使います。rxとryで角丸の大きさを指定できます。
正円的な形で描写したければrxとryに同じ値を指定します。

        <svg width="50" height="50">
            <rect x="10" y="10" width="30" height="30" rx="10" ry="5" />
        </svg>
20190109_9.png

SVGで多角形を描く

自由な形て多角形を描く場合にはpolygonタグを使います。
XとY座標をコンマ区切りで、次の座標を半角スペース区切りで指定します。($X_1,Y_1\ X_2,Y_2\ ...\ X_N,Y_N$といった具合に)
最後に始点の指定をしなくても閉じた形で描写されます。

        <svg width="50" height="50">
            <polygon points="25,10 40,22 35,40 15,40 10,22" />
        </svg>
20190109_10@.png

SVGで円を描く

circleタグを使います。rが半径、cxとcyが円の中心のX座標とY座標になります。その座標から上下左右に広がる形で描写されます。

        <svg width="50" height="50">
            <circle r="15" cx="25" cy="25" />
        </svg>
20190109_3.png

SVGで楕円を描く

ellipseタグを使います。座標指定は通常の円の時と同じですが、半径の指定がrxとryとなっており、X軸とY軸方向それぞれの半径を指定します。

        <svg width="50" height="50">
            <ellipse rx="23" ry="15" cx="25" cy="25" />
        </svg>
20190109_4.png

SVGで線を描く

lineタグを使います。
x1, y1の属性の座標から、x2, y2の座標に向けて線を描写します。
色はstroke属性で指定します。

        <svg width="50" height="50">
            <line x1="10" y1="10" x2="40" y2="40" stroke="#555555" />
        </svg>
20190109_5.png

SVGで折れ線を描く

polylineタグを使います。
points属性に、多角形のときと同様、XとY座標をコンマ区切りで、次の座標を半角スペース区切りで指定します。

        <svg width="50" height="50">
            <polyline points="10,10 25,10 25,40 40,40" style="fill: none; stroke: black" />
        </svg>
20190109_6.png

なお、ここではstyle属性で塗り(fill)にnoneを指定しています。これを指定しないと、折れ線に応じた領域に対してべた塗りが適用されてしまい、想定した結果になりません(Adobe PhotoshopやIllustrator、Animateなんかでペンツールというか、ベジェ曲線など扱ったことがある方はそれらをイメージすると分かりやすいかもしれません)。
スタイル関係に関しては後述します。

塗りにnoneを指定しない例 :

20190109_8.png

SVGでテキストを表示する

textタグを使います。

        <svg width="50" height="50">
            <text x="7" y="28">
                Apple
            </text>
        </svg>
20190109_12.png

基本的には、親のHTML要素のCSSのフォント設定などが引き継がれます。
もしくは、直接属性に指定したり、CSSで調整します。

        <svg width="50" height="50">
            <text x="12" y="28" font-family="VL Gothic" font-size="10">
                Apple
            </text>
        </svg>
20190109_11.png

SVGでのスタイル設定

※以降の例では、分かりやすさのためにタグにstyle属性で直接指定していますが、通常は別のCSSでクラスなど用意してください。

SVGの塗りの色の指定

CSSのfillの値に16進数やorangeといった文字列で指定します。
図形などの描写で塗りが不要な場合にはnoneを指定します(線だけ表示したい場合など)。

        <svg width="50" height="50">
            <circle style="fill: #eeeeee;" r="15" cx="25" cy="25" />
        </svg>
20190109_13.png

SVGの線の指定

CSSのstrokeの値に線の色、stroke-widthに線幅を指定します。

        <svg width="50" height="50">
            <circle style="stroke: #eeeeee; stroke-width: 3;" r="15" cx="25" cy="25" />
        </svg>
20190109_14.png

SVGのパス

pathタグを使います。ただ、ベジェ曲線・・的なものなど含め、少し複雑になってくるので、D3.jsの説明で少し述べて終わりにします。(ここでは省略)

SVGの透明度の指定

CSSのopacityで設定します。0.0で完全に透明、1.0で完全に不透明、0.5で半透明となります。

        <svg width="50" height="50">
            <circle style="opacity: 0.2;" r="15" cx="25" cy="25" />
        </svg>

※分かりやすいように、背景色を水色にし、黒の円を乗せています。

20190109_15.png

SVGの重なり順

基本的に後から追加したものが上に表示されます。
そのため、D3.jsで自分でプロットを作る際には軸の目盛りやラベルなどは図形を追加した後に追加するなどします。

左から順番に円を追加していく例 :

        <svg width="50" height="50">
            <circle style="fill: #222222;" r="10" cx="10" cy="25" />
            <circle style="fill: #444444;" r="10" cx="20" cy="25" />
            <circle style="fill: #666666;" r="10" cx="30" cy="25" />
            <circle style="fill: #888888;" r="10" cx="40" cy="25" />
        </svg>
20190109_16.png

SVGでのフィルターについて

ある程度触れますが、色々できます。CSSでもできるものも多いですが、一応触れておきます。

SVGでのぼかし(ガウス)

filterタグと、その子の要素としてfeGaussianBlurタグを使います。
それらのフィルタータグを定義した後に、ぼかしを反映したいオブジェクトでそのフィルターを指定します。
defsタグは定義のためのタグです。これはMDNのドキュメントが分かりやすかったので引用します。

SVGでは、後で再利用できるよう描画オブジェクトを定義します。参照される要素は、可能なかぎりdefs要素内で定義されることが推奨されています。defs要素内でこれらの要素を定義することは、SVGの要素の可読性を向上させ、ひいては操作性をも向上させます。defs要素の描画要素は、そのままでは描画されません。
defs MDN web docs

        <svg width="50" height="50">
            <defs>
                <filter id="gaussian-blur" filterUnits="userSpaceOnUse">
                    <feGaussianBlur in="SourceGraphic" stdDeviation="5" />
                </filter>
            </defs>
            <circle r="15" cx="25" cy="25" filter="url(#gaussian-blur)" />
        </svg>
  • filterUnits属性は、フィルター反映の座標や領域の指定の際に、どこを基準とするかの種別値を指定します。以下のいずれかを設定します。
    • userSpaceOnUse : 今回の例でいうとSVGの自体の座標と領域が基準になります。
    • objectBoundingBox : フィルターを設定するオブジェクト(今回は円)の座標とサイズが基準となります。デフォルトではこちらが設定されるようです。なお、ぼかしなどでオブジェクトの見える領域が大きくなる場合には、サイズ指定などを気を付けないとフィルターの効果が途切れたりします。
  • inには、どのようにオブジェクトを参照するのかの種別値を指定します。Photoshopの描画モードなんかに少し近いイメージ。色々あるものの、一部を例として載せておきます。
    • SourceGraphic : 対象となる要素(今回は円)のグラフィックの値(RGBA)が対象となります。
    • SourceAlpha : SourceGraphicとほぼ挙動は同じですが、こちらは透明度(Alpha)の値のみが対象となります。
  • stdDeviationの属性はぼかしの広がりを指定します。大きい値を指定するほど、なだらかに広くぼかされます。データサイエンスとかを勉強している方にはお馴染みの、標準偏差の分布の裾野の広さ的な指定になります。
  • 円に指定しているfilter属性では、url(セレクタ)という形式で対象のフィルターを指定します。今回はフィルター側にgaussian-blurというIDを設定しているのでそれを指定します。

stdDeviation="5"の例 :

20190109_17.png

stdDeviation="10"の例 :

20190109_18.png

ここで少しこういったフィルターが役に立つのか、ということを少し触れるため、SVGにおける画像の説明をしつつ脱線してみようと思います。

SVGの画像の扱いと、フィルターを組み合わせる

※画像をお借りしました : 10 Free Polygon Backgrounds

SVGでビットマップ画像を扱うには、imageタグを使います。(imgタグではないのでご注意を)
座標やサイズなどの基本的な設定に加えて、link:hrefで画像のリンク先を指定します。

        <svg width="600" height="370">
            <image x="0" y="0" width="100%" height="100%" xlink:href="./polygon_img.jpg" />
        </svg>

20190109_21.png

ここで、先ほどのSVGのフィルターやテキスト・ライン表示・CSS微調整を組み合わせてみます。
座標やテキストは適当です。(Wikipediaのものを参照したり等)

            text {
                fill: #ffffff;
            }

            line {
                stroke: #ffffff;
                opacity: 0.3;
            }
        <svg width="600" height="370">
            <defs>
                <filter id="gaussian-blur" filterUnits="userSpaceOnUse" x="0" y="141" height="141">
                    <feGaussianBlur in="SourceGraphic" stdDeviation="6" />
                </filter>
            </defs>
            <image x="0" y="0" width="100%" height="100%" xlink:href="./polygon_img.jpg" />
            <image x="0" y="0" width="100%" height="100%" xlink:href="./polygon_img.jpg" filter="url(#gaussian-blur)" />
            <line x1="0" y1="141" x2="670" y2="141" />
            <line x1="0" y1="282" x2="670" y2="282" />
            <text x="20" y="195" font-size="30">
                SVG and D3.js Beginner's Guide
            </text>
            <text x="20" y="250" font-size="10">
                D3.js is a JavaScript library for producing dynamic, interactive data visualizations in web browsers.
            </text>
        </svg>

※フィルターを設定すると、設定した画像がフィルター領域しか表示されないので、同じ画像を2つ表示(フィルター未反映版とフィルター反映版)していますが、もし別の良さそうなやり方がありましたらご指摘ください。

20190109_19.png

なんかそれっぽい表現になりました(雑)。SVGで色々扱うことで、例えばブログの記事のバナー画像で、毎回Photoshopなどで加工して・・とせずとも、画像のアップだけであとは自動的にぼかしフィルターや線の追加がされる、といったようにしても便利かもしれません。

また、これらのフィルターの設定、たとえばぼかし量などをインタラクティブにアニメーションさせたりしても面白い表現になります。マウスが乗った要素のみフィルターのぼかし量を0にし、その他はぼかしが入るようにするなど。
アニメーション表現に関してはD3.jsの説明で追加で色々触れます。

色々とアニメーション表現の幅が広がるので、次節で他のフィルターも見てみます。

SVGでの彩度調整

画像をグレースケールにしたり、逆に数値を戻すことで元のカラー画像にしたりできます。
feColorMatrixタグを使います。ぼかしの時に使った、filterUnitsとかinは不要です。typeにsaturate、valuesに0を指定することで、グレースケールになります。valuesは、0.0~1.0で指定し、0で完全にグレースケール、0.5などで低彩度な画像になります。(saturation=彩度)

values="0"のサンプル :

        <svg width="300" height="185">
            <defs>
                <filter id="grayscale">
                    <feColorMatrix type="saturate" values="0" />
                </filter>
            </defs>
            <image x="0" y="0" width="300" height="185" xlink:href="./polygon_img.jpg" filter="url(#grayscale)" />
        </svg>

20190112_1.png

values="0.5"のサンプル :

20190112_2.png

SVGでの色相変換

彩度が変更できるなら、色相もいけるでしょ・・ということで、もちろん用意されているようです。
タグは先ほどと同じfeColorMatrixで、typeにhueRotateを指定します。hueが色相という意味で、hueRotateで色相環といった感じでしょうか。valuesは0~359などで指定します。0で元の画像から変わりません(変更していた場合には元に戻ります)。180で対角にある色になります。

        <svg width="300" height="185">
            <defs>
                <filter id="hue-rotate">
                    <feColorMatrix type="hueRotate" values="180" />
                </filter>
            </defs>
            <image x="0" y="0" width="300" height="185" xlink:href="./polygon_img.jpg" filter="url(#hue-rotate)" />
        </svg>

20190112_3.png

描画モード

Adobeなどのデザインツールを使っている方にはお馴染みの描画モード(blend mode)。
画像を重ねた際の見え方で色々遊べます。自然な影の表現だったり、光のエフェクト表現など。
よく使われる加算の描画モードは、覆い焼き(color-dodge)が該当します。
feBlendとかのフィルターもある一方で、扱う際にはCSSのmix-blend-modeなどを使う方が記述が楽そうではあります。ただ、IEやedgeでまだサポートされていないそうで。edgeがChromiumベースのものに差し変わったら、改善しそうではあります。IEなどをサポートする想定の場合は要注意でしょうか。

左から同じグレーの四角を、通常の描画モード、乗算(multiply)、スクリーン(screen)、焼きこみカラー(color-burn)、覆い焼き(color-dodge)で指定した例 :

            rect {
                fill: #aaaaaa;
            }

            .multiply {
                mix-blend-mode: multiply;
            }

            .screen {
                mix-blend-mode: screen;
            }

            .color-burn {
                mix-blend-mode: color-burn;
            }

            .color-dodge {
                mix-blend-mode: color-dodge;
            }
        <svg width="300" height="185">
            <image x="0" y="0" width="300" height="185" xlink:href="./polygon_img.jpg" />
            <rect x="10" y="75" width="35" height="35" />
            <rect class="multiply" x="55" y="75" width="35" height="35" />
            <rect class="screen" x="100" y="75" width="35" height="35" />
            <rect class="color-burn" x="145" y="75" width="35" height="35" />
            <rect class="color-dodge" x="190" y="75" width="35" height="35" />
        </svg>

20190112_4.png

SVGでのグループ要素の設定

SVG内でコンテナーとしてgタグ(グループ)を使うことができます。
gタグにスタイルやフォント、フィルターなどを設定すると、内部に格納される複数の要素に一括してそれらの設定を反映したりすることができます。
D3.jsで、例えば各軸で要素が大量に生成される際などにグループにまとめる目的で利用すると、制御がシンプルになり便利です。

グループに灰色の塗りを設定した結果、内部の四角に一通りその塗りが反映される例 :

        <svg width="300" height="185" style="background-color: #333333;">
            <g fill="#aaaaaa">
                <rect x="10" y="75" width="35" height="35" />
                <rect x="55" y="75" width="35" height="35" />
                <rect x="100" y="75" width="35" height="35" />
            </g>
        </svg>
20190114_1.png

D3.jsでの要素の選択と追加・テキストの設定

基本的な操作はjQueryのように使えます。DOM要素の単体の選択はselect、条件を満たす要素全ての選択にはselectAllメソッドを使います。要素の追加には、appendメソッドを使い、要素内へのテキストの追加はtextメソッドを使います。
また、D3.jsでは記述をメソッドチェーンで記述する形がかなり多くなります。

bodyタグを選択し、その中にpタグの要素を追加する例 :

        <script type="text/javascript">
            d3.select("body").append("p").text("Sample text.");
        </script>

D3.jsでのCSV読み込み

csvメソッドを使います。4系では第一引数にCSVのパス、第二引数にコールバックとして無名関数を指定します。無名関数の第一引数には、CSVの内容の辞書を格納した2次元の配列が設定されます。

sample_data.csv :

name,price
Apple,100
Orange,120
Melon,230
        <script type="text/javascript">
            d3.csv("./sample_data.csv", function(data) {
                console.log(data);
            });
        </script>

5系ではコールバックではなく、Promisesが返却されるので少しだけ書き方が変わります。(数少ない4系と5系で互換性がない点の一つ)

        <script type="text/javascript">
            d3.csv("./sample_data.csv").then(function(data) {
                console.log(data);
            });
        </script>

webコンソールでデータを見てみると、CSVの内容に応じた内容が読み込まれていることを確認できます。

20190113_1.png

データの中には、columnsというキーの値もあり、PythonのPandasのデータフレームのcolumns属性のように、データのカラム名のリストが格納されます。

データの変換処理を挟む

CSVで読み込みだデータで、うっかり数値の想定だったところが文字列になっていたりすると困るので数値に変換しておきたいとか、もしくは日付の文字列をDateオブジェクトに変換しておきたいといった指定が必要な場合には、変換処理用の関数を挟むことができます。
(通常、数値の個所でも文字列の形になったりするようです。前述の節のスクショの、priceカラムの部分がダブルクォーテーションで囲まれている点からも分かります)
その場合、csv関数の第二引数が変換用の関数、第三引数がコールバックといったような指定となります。(4系の場合)

変換用の関数の第一引数には1行分のデータの辞書が渡されます。このデータは慣習的にdという引数名が指定されることが多いそうです。
返却値に、返却後の辞書を指定します。今回はCSVのpriceカラムの部分にparseIntを挟んで、整数として扱われるように調整しました。

        <script type="text/javascript">
            var rowConvertFunc = function(d) {
                return {
                    name: d.name,
                    price: parseInt(d.price)
                };
            }
            d3.csv("./sample_data.csv", rowConvertFunc, function(data) {
                console.log(data);
            });
        </script>
20190113_2.png

priceカラムの部分がダブルクォーテーションで囲まれていたのが、整数になっているのが分かります。

CSV読み込み時のエラーハンドリング

コールバックの第一引数をエラーのオブジェクト、第二引数をデータという指定にすると、CSV読み込み時の制御ができます。

        <script type="text/javascript">
            var rowConvertFunc = function(d) {
                return {
                    name: d.name,
                    price: parseInt(d.price)
                };
            }
            d3.csv("./sample_data.csv", rowConvertFunc, function(error, data) {
                if (error) {
                    // エラーが存在する場合の処理を記述。
                }else {
                    // 正常に読み込めた際の処理を記述。
                }
            });
        </script>

CSV以外のフォーマットの読み込み

d3.csvメソッドの変わりにd3.tsvやd3.jsonといったメソッドが用意されているので、それらを利用します。使い方はcsvのものと大体一緒です。

D3.jsでの基本的なデータセットに対するループの処理

  • D3.jsでは、表示したいデータセットに応じて、一つ一つ(1行1行)データを要素にバインドしていったりスタイルなどの設定を回していく形でプロットを作っていきます。
    基本的な流れは、selectAll・data・enter・append・そのほか必要なメソッド といったような流れで記述します。
  • selectAllに関して、最初は「存在しない要素」に対して、追加する要素を指定をします。なんだか最初大分違和感がありましたが、深く考えずに「そういうもの」と考えると良さそうです。
  • dataメソッドでは、データセットとなる配列もしくは辞書などを指定します。指定されたデータの件数に応じて空のplaceholder的な要素が準備されます。
  • enterメソッドは、「準備された空の要素に対して、1つ1つ順番にデータをバインドしていく」といった処理になります。dataメソッドで用意された要素の件数(=データセットの件数)分実行されます。
  • 次にデータがバインドされた要素に対してappendメソッドなどで任意のHTML要素を追加します。
  • appendメソッドなどで追加した要素に対して、表示やアニメーションなどの設定を行います。

D3.jsの基本的な流れはこんな感じになります。

enterとは逆に操作などでデータセットを減らしたものの用意した要素に余分なものが残っている場合に、データセットと要素の件数を合わせる(要素の削除を行う)際に使うexitというメソッドもありますが、そちらは後述します。

初見の際に、少しこの流れが違和感がありましたが、最初はあまり深く考えずにこの流れで記述する、と覚えておけば十分かもしれません。
少し慣れてきたら、深追いしていくとより理解が深まって好ましいかも、とは思います。
(参考 【D3.js】超基本! コンソールでselect,data,enterメソッドを理解する。

サンプルとして、4件の数値を格納した配列を指定する形で試してみます。

        <script type="text/javascript">
            var dataset = [5, 10, 15, 20];
            d3.select("body")
                .selectAll("p")
                .data(dataset)
                .enter()
                .append("p")
                .text("サンプルテキスト");
        </script>

for文などは必要ありませんが、内部ではデータセットのデータ1件ごとにappendやtextなどのメソッドチェーンが繰り返し実行される形になります。

20190113_3.png

HTMLで表示してみても、for文のように繰り返し要素の追加やテキスト設定などが実行されていることが分かります。

D3.jsで、実際のデータにアクセスしていく

データをバインドして、ループ的に処理が回っていくことは確認できました。
次に実際にデータセットの中身にアクセスしていきます。

D3.jsでは、メソッドチェーン中の各メソッドの第一引数などに、固定のパラメーターとは別に無名関数を指定することができます。
例えば、前述のサンプルで言えばtextメソッドで固定の文字列を指定する代わりに、無名関数を指定することができます。
無名関数の第一引数には、データ単体が渡されます。(CSVの読み込みの節で少し触れた、dという変数名によるデータ)

        <script type="text/javascript">
            var dataset = [5, 10, 15, 20];
            d3.select("body")
                .selectAll("p")
                .data(dataset)
                .enter()
                .append("p")
                .text(function(d) {
                    return "データは" + d + "です。";
                });
        </script>
20190113_4.png

D3.jsでのスタイル調整

追加した要素に対するスタイル設定も、メソッドチェーンに追加していくだけでさくっと対応ができます。
styleというメソッドが用意されており、第一引数にCSSのプロパティ名、第二引数に値を設定します。jQueryのcssメソッドのような感覚です。

文字に水色のスタイルを設定する例。

        <script type="text/javascript">
            var dataset = [5, 10, 15, 20];
            d3.select("body")
                .selectAll("p")
                .data(dataset)
                .enter()
                .append("p")
                .text(function(d) {
                    return "データは" + d + "です。";
                })
                // 以下のスタイル設定を追加。
                .style("color", "#00aaff");
        </script>
20190113_5.png

こちらも、第二引数の値を無名関数にすることで、データセットのデータにアクセスすることもできます。これによって、データセットの数値に応じてプロットの要素のサイズや色・座標を設定したりできるようになります。

        <script type="text/javascript">
            var dataset = [5, 10, 15, 20];
            d3.select("body")
                .selectAll("p")
                .data(dataset)
                .enter()
                .append("p")
                .text(function(d) {
                    return "データは" + d + "です。";
                })
                .style("color", "#00aaff")
                // データを参照して、文字サイズを設定します。
                .style("font-size", function(d) {
                    return d + "px";
                });
        </script>
20190113_6.png

D3.jsの各無名関数の第二引数にはインデックスが渡されるようにできる

データセットに応じて、一つ一つ設定を行う際に、対象のデータが何番目のデータなのかを知りたいことがあります。例えば、棒グラフで左から棒を並べる際に、インデックス番号に応じてX座標を設定していくようなケースです。
その場合、各無名関数の第二引数(データのdの次の引数)にインデックスの引数を受け取るように設定することができます。

        <script type="text/javascript">
            var dataset = [5, 10, 15, 20];
            d3.select("body")
                .selectAll("p")
                .data(dataset)
                .enter()
                .append("p")
                .text(function(d, i) {
                    return "データは" + d + "、インデックスは" + i + "です。";
                })
                .style("color", "#00aaff")
                // iの引数に、データのインデックス(0~)を受け取る
                // ことができます。
                .style("font-size", function(d, i) {
                    return (i * 5 + 5) + "px";
                });
        </script>
20190113_7.png

D3.jsでクラスを設定する

メソッドチェーン中でattrメソッドを使うことで、対象の要素の属性を調整することができます。
これを利用して、class属性を追加したりもできますが、D3.js側でclassedメソッドが用意されているので、こちらを使うとよりシンプルです。第一引数に設定したいクラス名、第二引数に真偽値で、クラスを設定(true)するのか設定を外す(false)を指定します。

        <script type="text/javascript">
            var dataset = [5, 10, 15, 20];
            d3.select("body")
                .selectAll("p")
                .data(dataset)
                .enter()
                .append("p")
                .text(function(d, i) {
                    return "データは" + d + "、インデックスは" + i + "です。";
                })
                // クラスを設定(もしくは解除)する際には、attrメソッドでも可能ですが、
                // classedメソッドを使うとよりシンプルです。
                .classed("bold-text", true);
        </script>

SVGをD3.jsを経由して追加する

HTML要素をD3.jsで追加する方法を前述しました。
SVGも例外ではなく、同じ流れで追加することができます。(ただし、SVG自体にはデータセットは設定する必要はないため、enterなどのメソッドは使用せずに直接appendなどを指定しています。)

        <script type="text/javascript">

            var width = 500;
            var height = 100;
            var svg = d3.select("body")
                .append("svg")
                .attr("width", width)
                .attr("height", height)
                .style("background-color", "#333333");
        </script>
20190113_8.png

指定したサイズ・背景色のSVGを追加できました。

幅だったり高さなどは、後々のレイアウトやアニメーションなどで参照したりするので、変数なり定数に設定しておくと良いと思われます。

幅や高さを、こちらもメソッドチェーンで指定しています。
また、このSVG要素は後で他の要素を追加するために参照するために、返却値を利用して変数に格納しておきます。
返却される値は、メソッドチェーン内で最後にappendなどで追加した要素が返ります。たとえば、上記のサンプルのメソッドチェーンにさらに加えてrect要素を追加した場合などには、rect要素の配列などが返却されたりします。

SVG内にD3.jsで任意の要素を追加する

特に新しいことはなく、今までで触れてきたメソッドなどを組み合わせて利用するのみです。

グレーの四角をデータセットの件数分追加を行うサンプル :

        <script type="text/javascript">

            var width = 500;
            var height = 100;
            var svg = d3.select("body")
                .append("svg")
                .attr("width", width)
                .attr("height", height)
                .style("background-color", "#333333");

            // 四角をデータセットの4件分に応じて追加します。
            var dataset = [5, 10, 15, 20];
            var rectList = svg.selectAll("rect")
                .data(dataset)
                .enter()
                .append("rect")
                .attr("x", function(d, i) {
                    return i * 100;
                })
                .attr("y", 20)
                .attr("width", function(d, i) {
                    return d * 3;
                })
                .attr("height", function(d, i) {
                    return d * 3;
                })
                .attr("fill", "#aaaaaa");
        </script>
20190113_9.png

D3.jsで棒グラフっぽい表示をしてみる

ここまでに触れてきたものの組み合わせで大体いけます。
X座標にはインデックスの番号に応じて、棒の幅(+余白の小さい値)を指定し、Y座標にはSVGの高さ - 棒の高さを指定しています。
棒グラフでは下部の位置は合わせて、上の方に伸ばしていくのが普通なのでこのようなY座標の指定にしています。

        <script type="text/javascript">

            const WIDTH_SVG = 500;
            const HEIGHT_SVG = 260;
            const WIDTH_BAR = 15;
            const MARGIN_X_BAR = 1;

            var svg = d3.select("body")
                .append("svg")
                .attr("width", WIDTH_SVG)
                .attr("height", HEIGHT_SVG)
                .style("background-color", "#333333");

            var dataset = [
                150, 96, 59, 50, 73, 119, 175, 223, 248, 242,
                208, 155, 100, 61, 50, 69, 114, 170, 219, 247,
                244, 212, 160, 105, 64, 50, 66, 109, 164, 215];
            var rectList = svg.selectAll("rect")
                .data(dataset)
                .enter()
                .append("rect")
                // 最初のインデックス以外は、棒の幅 * インデックス
                // + 余白のピクセルでX座標を設定。
                .attr("x", function(d, i) {
                    if (i == 0) {
                        return 0;
                    }
                    return i * (WIDTH_BAR + MARGIN_X_BAR);
                })
                // 棒の下の位置を揃えるために、SVGの高さ - 棒の高さ
                // で指定。
                .attr("y", function(d) {
                    return HEIGHT_SVG - d;
                })
                .attr("width", function(d, i) {
                    return WIDTH_BAR;
                })
                .attr("height", function(d, i) {
                    return d;
                })
                .attr("fill", "#dddddd");
        </script>
20190113_10.png

少しグラフっぽさが出てきました。

SVGの領域内にぴったりフィットするように、Y軸にD3.jsのscaleを使う

前述のプロットでは、高さの値はデータセットの値をそのまま指定しました。
しかしこれでは、データセットにもっと大きな値を指定するとSVGの領域を超えて見切れてしまいます。

この点は、D3のscaleという機能を使うとさくっと解決できます。
scaleは、データ領域の最小値と最大値を指定し、さらに出力される値の最小値と最大値を指定することで、値の範囲を制御することができます。

たとえば、データセットが0~200の値を取り、その値に応じてワードクラウドの円のプロット要素単体のサイズを30~50の範囲で設定したいとします。
その場合、データが0であればプロットサイズは30が必要で、データが200であればプロットサイズは50、データが半分の100であればプロットサイズは40が必要、といったマッピング的な処理が必要になります。
このマッピングの処理を扱うのがD3.jsのscaleです。今回はscaleLinearというscaleを使います。これは、マッピング後の値の推移が線形(比例)に変動していく、よく使われる基本的なscaleです。他にも指数的な変動をしていくものやDateオブジェクトを使ったscaleなど、色々用意されていますがここでは触れません。

データ領域をdomainメソッドで最小値、最大値の配列で指定し、出力のデータをrangeメソッドでこちらも最小値と最大値の2件の配列で指定します。作成したscaleのオブジェクトに、domainメソッドで指定した範囲内での値を指定すると、必要な出力値を得ることができます。

先ほどの例でいうと、以下のようになります。

            const DATASET_MIN = 0;
            const DATASET_MAX = 200;
            const PLOT_SIZE_MIN = 30;
            const PLOT_SIZE_MAX = 50;
            var scale = d3.scaleLinear()
                .domain([DATASET_MIN, DATASET_MAX])
                .range([PLOT_SIZE_MIN, PLOT_SIZE_MAX]);

            // 30が返却される。
            scale(0);
            // 50が返却される。
            scale(200);
            // 40が返却される。
            scale(100);

ブラウザのコンソールなどで、実際に上記のコードを実行してみると、想定した値が返却されることが確認できます。

20190113_11.png

今回はこれを利用して、domain側には0~データセットの最大値、range側には0~SVG全体の高さ - 若干の余白値 として設定して、heightScaleという変数名で棒の高さを取得するためのscaleを用意しました。1次元の配列のデータセット内の最大値を算出するには、D3.jsでmaxというメソッドが用意されているのでそちらを利用します。(minなども同様に存在します)

            var heightScale = d3.scaleLinear()
                .domain([0, d3.max(dataset)])
                .range([0, HEIGHT_SVG - MARGIN_TOP])

range側の最小値をデータセットの最小値ではなく0としているのは、棒グラフでは軸の最小値を0としないと、誤解を与えかねないという点を加味して0としています。(軸の最小値が大きく、数値的には大した変動ではないのにプロット上の表示は大きく差があるように見えてしまうなど)

作成したheightScaleオブジェクトを棒のY座標や高さを設定するところに設定すると以下のようになります。

        <script type="text/javascript">

            const WIDTH_SVG = 500;
            const HEIGHT_SVG = 260;
            const WIDTH_BAR = 15;
            const MARGIN_X_BAR = 1;
            // 上部に少しだけ余白を設けた方が見栄えがいいので、
            // その分の余白設定。
            const MARGIN_TOP = 10;

            var svg = d3.select("body")
                .append("svg")
                .attr("width", WIDTH_SVG)
                .attr("height", HEIGHT_SVG)
                .style("background-color", "#333333");

            var dataset = [
                150, 96, 59, 50, 73, 119, 175, 223, 248, 242,
                208, 155, 100, 61, 50, 69, 114, 170, 219, 247,
                244, 212, 160, 105, 64, 50, 66, 109, 164, 215];

            // 棒の高さを、SVGのサイズとデータセットの最大値を加味して
            // 調整されるようにscaleオブジェクトを用意します。
            var heightScale = d3.scaleLinear()
                .domain([0, d3.max(dataset)])
                .range([0, HEIGHT_SVG - MARGIN_TOP])

            var rectList = svg.selectAll("rect")
                .data(dataset)
                .enter()
                .append("rect")
                .attr("x", function(d, i) {
                    if (i == 0) {
                        return 0;
                    }
                    return i * (WIDTH_BAR + MARGIN_X_BAR);
                })
                // 棒の高さの部分を、用意したheightScaleを挟んだ
                // 値を指定することで、SVG領域にフィットする値になります。
                .attr("y", function(d) {
                    return HEIGHT_SVG - heightScale(d);
                })
                .attr("width", function(d, i) {
                    return WIDTH_BAR;
                })
                .attr("height", function(d, i) {
                    return heightScale(d);
                })
                .attr("fill", "#dddddd");
        </script>
20190113_15.png

試しに、データセットの最初の3件の値を10倍にして、ちゃんとSVGの領域にフィットすることを確認してみます。

20190113_14.png

値を変えても、ちゃんとSVGとデータセットに合わせてスケールすることができました。

X軸もSVGの領域にフィットするようにする

X軸も同様で、データセットの件数の増減に応じて、棒の幅を調整したいところです。
こちらはSVG領域の幅をデータセットの件数を除算すれば算出できます。


const WIDTH_BAR = WIDTH_SVG / dataset.length - MARGIN_X_BAR;
20190113_16.png

D3.jsで軸を設定する

少し考慮すべき点が多くなるので、先にどんなものを作るのか画像を貼っておきます。

20190114_2.png

上記のように、D3.jsの機能を使って左端に軸を追加します。
いくつか以下の加味すべき点があるため、順番に見ていきます。

  1. 軸の0のテキスト部分など、プロットよりもわずかに下に来るので、周りに余白を設けないと見切れてしまう。
  2. 軸の下限と上限はデータセットの内容を加味し、且つ棒の高さに合わせる必要がある。
  3. 軸のテキストの桁はデータセットに応じて変動するので、レイアウトは固定値ではなく軸の要素の幅に応じて動的に設定する必要がある。

まず1ですが、これは余白の定数を設けてた後に、棒の四角のY座標や高さの計算をその余白を加味したものにすれば解決します。今回は10pxとしました。


            const MARGIN_PLOT_OUTSIDE = 10;
...
            var rectList = svg.selectAll("rect")
                .data(dataset)
                .enter()
                .append("rect")
                .attr("height", function(d, i) {
                    return heightScale(d);
                })
                .attr("y", function(d) {
                    return HEIGHT_SVG - heightScale(d) - MARGIN_PLOT_OUTSIDE;
                })
                .attr("fill", "#dddddd");

※後でコード全体を載せるので、そちらも合わせてご確認ください。

次に、2のデータセットの数値などを加味した軸の作成です。
軸の作成はD3.jsのaxisTop、axisRight、axisBottom、axisLeftというメソッドで必要なものを作成できます。
今回は左側に配置する軸を作成したいので、axisLeftを使います。
また、データセットの数値に合わせた軸設定をするために、軸用のスケールのオブジェクトを用意します。

            var axisScale = d3.scaleLinear()
                .domain([0, d3.max(dataset)])
                .range([HEIGHT_SVG - MARGIN_PLOT_OUTSIDE, MARGIN_PLOT_OUTSIDE]);

domain側に0~データセットの最大値を設定し、range側にSVG領域を加味した範囲を設定しています。
Y軸の座標は下に行くほど値が大きくなる一方で、棒グラフの値は下の方が小さい(0に近づいていく)という点に注意してください。
つまり、データセットの値が0に近いほどマッピングされるY座標の値は大きくなり、データセット
の値が最大値に近づくほどY座標は小さな値になっていきます。

軸の生成用のオブジェクトをaxisLeftを使って用意します。このオブジェクト自体は軸の表示用の要素ではなく、このオブジェクトを呼び出した時点で軸の線やテキストが一気に生成されます。
scaleメソッドで用意したスケールオブジェクトを指定できます。

            var yAxis = d3.axisLeft()
                .scale(axisScale);

準備ができたので、SVGに軸を追加しましょう。
様々な要素が一気に生成されるので、グループをSVGに追加して、そのグループに軸要素を追加しておきます。こうすることで、軸の一括の操作などがしやすくなったり、軸のサイズの領域が取りやすくなったりします。
yAxisの変数を呼び出す(yAxis()といったように)と軸の線やテキストが生成されますが、メソッドチェーン内でそういった記述をするにはcallメソッドを利用します。
また、軸のスタイル設定用に、axisというクラス名を設定しています。

            var axisGroup = svg.append("g")
                .classed("axis", true)
                .call(yAxis);

スタイルも調整しておきます。軸はpathとlineとtextの3要素から構成されるため、それらに対して色などのスタイルを調整しておきます。

            .axis path,
            .axis line {
                stroke: #aaaaaa;

                /* cripsEdgesを指定すると、ピクセル数が小数などの中途半端な
                値でぼやけて見栄えが悪くなるのを防止できる。*/
                shape-rendering: crispEdges;
            }

            .axis text {
                font-family: Arial, Helvetica, sans-serif;
                fill: #aaaaaa;
            }

これでSVG内に追加されました。ただ、X座標が縦の線の部分がなっていて、数値のテキスト部分がマイナスのX座標に配置された状態で生成されるので、この時点ではほとんどSVG上では見えません。
X座標を右にずらす必要がありますが、その際に3の問題で、軸のテキストはデータセットに応じて桁が変動する可能性があり、軸要素の幅を加味して設定する必要があります。
要素のサイズの取得には、要素.node().getBBox()とすることで、要素の座標やサイズなどを格納したオブジェクトを取得できるのでそちらを利用します。(デザインツールで言うところのバウンディングボックスですね)

            var axisBBox = axisGroup.node()
                .getBBox();

取得したバウンディングボックスのオブジェクトに対して、xやwidthなどにアクセスすると、座標やサイズなどが取得できます。余白の値と軸の幅を加味して、軸のX座標を決めます。

            var axisX = MARGIN_PLOT_OUTSIDE + axisBBox.width;

算出したX座標を軸に反映します。座標の調整には、要素のtransform属性に対して、translate(X座標, Y座標)という指定をすると設定ができます。

            axisGroup.attr("transform", "translate(" + axisX + ", 0)");

最後に、棒グラフの棒のX座標と幅の指定をしておきます。
棒自体の生成やY座標などの設定は事前にしてありますが、X座標や幅は軸を生成した後じゃないと棒を配置する領域が算出できないため、軸生成後に設定しています。
追加自体は、軸の方が上に来る方が自然なため、先に追加してあります。(今回のプロットでは、棒と軸が被るような構成ではありませんが)

            const WIDTH_BAR = (WIDTH_SVG - axisX - MARGIN_PLOT_OUTSIDE * 2) / dataset.length - MARGIN_X_BAR;
            rectList
                .attr("x", function(d, i) {
                    var x = axisX + MARGIN_PLOT_OUTSIDE;
                    if (i == 0) {
                        return x;
                    }
                    x += i * (WIDTH_BAR + MARGIN_X_BAR);
                    return x;
                })
                .attr("width", function(d, i) {
                    return WIDTH_BAR;
                });

これで軸の基本的な対応は完成です!実際に使う際には軸を下側に配置したりするのも必要になってくることがほとんどだと思うため、同じような流れでaxisBottomなどを使って生成してください。

**[クリックでコード全体を表示する]**
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script type="text/javascript" src="./d3.js"></script>
    </head>
    <body>
        <style>
            .axis path,
            .axis line {
                stroke: #aaaaaa;

                /* cripsEdgesを指定すると、ピクセル数が小数などの中途半端な
                値でぼやけて見栄えが悪くなるのを防止できる。*/
                shape-rendering: crispEdges;
            }

            .axis text {
                font-family: Arial, Helvetica, sans-serif;
                fill: #aaaaaa;
            }
        </style>
        <script type="text/javascript">

            const WIDTH_SVG = 500;
            const HEIGHT_SVG = 260;
            const MARGIN_X_BAR = 1;
            const MARGIN_PLOT_OUTSIDE = 10;

            var svg = d3.select("body")
                .append("svg")
                .attr("width", WIDTH_SVG)
                .attr("height", HEIGHT_SVG)
                .style("background-color", "#333333");

            var dataset = [
                150, 96, 59, 50, 73, 119, 175, 223, 248, 242,
                208, 155, 100, 61, 50, 69, 114, 170, 219, 247,
                244, 212, 160, 105, 64, 50, 66, 109, 164, 215];

            var heightScale = d3.scaleLinear()
                .domain([0, d3.max(dataset)])
                .range([0, HEIGHT_SVG - MARGIN_PLOT_OUTSIDE * 2])

            var rectList = svg.selectAll("rect")
                .data(dataset)
                .enter()
                .append("rect")
                .attr("height", function(d, i) {
                    return heightScale(d);
                })
                .attr("y", function(d) {
                    return HEIGHT_SVG - heightScale(d) - MARGIN_PLOT_OUTSIDE;
                })
                .attr("fill", "#dddddd");

            // 軸の値の表記をデータセットに合わせた値とするため、
            // 軸用のスケールオブジェクトを用意します。
            var axisScale = d3.scaleLinear()
                .domain([0, d3.max(dataset)])
                .range([HEIGHT_SVG - MARGIN_PLOT_OUTSIDE, MARGIN_PLOT_OUTSIDE]);

            // yAxisは設定に応じた軸要素生成用のオブジェクトです。
            // この時点では軸の内容は未生成で、yAxisを呼び出した時点で
            // 線やテキストなど様々な要素が生成されます。
            var yAxis = d3.axisLeft()
                .scale(axisScale);

            // 線やテキストなど様々な要素が生成されるため、一括で扱える
            // ようにグループを挟んでいます。call(yAxis)の記述でyAxisが
            // 呼び出され、様々な要素が返却されグループに追加されます。
            var axisGroup = svg.append("g")
                .classed("axis", true)
                .call(yAxis);

            // 要素.node().getBBox()で対象の要素のバウンディング
            // ボックスが取得できます。座標やサイズなどが格納されています。
            // xやwidthといったキーでアクセスができます。
            var axisBBox = axisGroup.node()
                .getBBox();

            // 軸の幅を加味して、軸のX座標を設定します。
            var axisX = MARGIN_PLOT_OUTSIDE + axisBBox.width;
            axisGroup.attr("transform", "translate(" + axisX + ", 0)");

            const WIDTH_BAR = (WIDTH_SVG - axisX - MARGIN_PLOT_OUTSIDE * 2) / dataset.length - MARGIN_X_BAR;
            rectList
                .attr("x", function(d, i) {
                    var x = axisX + MARGIN_PLOT_OUTSIDE;
                    if (i == 0) {
                        return x;
                    }
                    x += i * (WIDTH_BAR + MARGIN_X_BAR);
                    return x;
                })
                .attr("width", function(d, i) {
                    return WIDTH_BAR;
                });
        </script>
    </body>
</html>

D3.jsの軸の刻みの件数を調整する

軸をどのくらい細かく刻むかどうかの調整はticksメソッドをaxisLeftなどのオブジェクトで設定するだけです。他と同様、メソッドチェーン内で設定します。

            var yAxis = d3.axisLeft()
                .scale(axisScale)
                .ticks(5);

5を設定すれば「5件くらいに刻まれる」制御となります。「くらい」と表現した点に注意してください。確実に5件とはならず、データセットの内容に応じて人が見て良さそうな感じの件数に調整されます。4件かもしれませんし、6件になるかもしれません。

20190114_3.png

先ほどよりも大分すっきりした軸の表示になりました。

等比ではなく、直接刻みの値を軸に設定する

重要な値を確実に表示したい場合など、軸の刻みを任意の値の配列で直接指定したい場合にはtickValuesを使います。

            var yAxis = d3.axisLeft()
                .scale(axisScale)
                .tickValues([0, 100, 120, 160, 180, 240]);
20190114_4.png

D3.jsでのイベントリスナー設定

インタラクティブなプロットを作るためのイベントリスナー設定も、jQueryのような感覚のものがD3.jsでも用意されています。
jQuery同様、onメソッドが用意されているので、それをメソッドチェーン中に追加していきます。
onメソッドの第一引数にはイベントの種別(clickやmouseover、startなど様々)を指定し、第二引数にはイベントハンドラ用の無名関数を指定します。
イベントハンドラ用の無名関数内で、$this的な要素の選択の代わりに、d3.select(this)とすることで、対象の要素の参照を得ることができます。

クリックされた四角の色を赤くする例 :

            var rectList = svg.selectAll("rect")
                .data(dataset)
                .enter()
                .append("rect")
                .attr("height", function(d, i) {
                    return heightScale(d);
                })
                .attr("y", function(d) {
                    return HEIGHT_SVG - heightScale(d) - MARGIN_PLOT_OUTSIDE;
                })
                .attr("fill", "#dddddd")
                // 四角の要素がクリックされた際に色が赤くなるような挙動を
                // 設定します。
                .on("click", function() {
                    var targetRect = d3.select(this);
                    targetRect.attr("fill", "#cc3333");
                });
20190114_5.png
**[クリックでコード全体を表示する]**
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script type="text/javascript" src="./d3.js"></script>
    </head>
    <body>
        <style>
            .axis path,
            .axis line {
                stroke: #aaaaaa;

                /* cripsEdgesを指定すると、ピクセル数が小数などの中途半端な
                値でぼやけて見栄えが悪くなるのを防止できる。*/
                shape-rendering: crispEdges;
            }

            .axis text {
                font-family: Arial, Helvetica, sans-serif;
                fill: #aaaaaa;
            }
        </style>
        <script type="text/javascript">

            const WIDTH_SVG = 500;
            const HEIGHT_SVG = 260;
            const MARGIN_X_BAR = 1;
            const MARGIN_PLOT_OUTSIDE = 10;

            var svg = d3.select("body")
                .append("svg")
                .attr("width", WIDTH_SVG)
                .attr("height", HEIGHT_SVG)
                .style("background-color", "#333333");

            var dataset = [
                150, 96, 59, 50, 73, 119, 175, 223, 248, 242,
                208, 155, 100, 61, 50, 69, 114, 170, 219, 247,
                244, 212, 160, 105, 64, 50, 66, 109, 164, 215];

            var heightScale = d3.scaleLinear()
                .domain([0, d3.max(dataset)])
                .range([0, HEIGHT_SVG - MARGIN_PLOT_OUTSIDE * 2])

            var rectList = svg.selectAll("rect")
                .data(dataset)
                .enter()
                .append("rect")
                .attr("height", function(d, i) {
                    return heightScale(d);
                })
                .attr("y", function(d) {
                    return HEIGHT_SVG - heightScale(d) - MARGIN_PLOT_OUTSIDE;
                })
                .attr("fill", "#dddddd")
                // 四角の要素がクリックされた際に色が赤くなるような挙動を
                // 設定します。
                .on("click", function() {
                    var targetRect = d3.select(this);
                    targetRect.attr("fill", "#cc3333");
                });

            var axisScale = d3.scaleLinear()
                .domain([0, d3.max(dataset)])
                .range([HEIGHT_SVG - MARGIN_PLOT_OUTSIDE, MARGIN_PLOT_OUTSIDE]);

            var yAxis = d3.axisLeft()
                .scale(axisScale)
                .ticks(5);

            var axisGroup = svg.append("g")
                .classed("axis", true)
                .call(yAxis);

            var axisBBox = axisGroup.node()
                .getBBox();

            var axisX = MARGIN_PLOT_OUTSIDE + axisBBox.width;
            axisGroup.attr("transform", "translate(" + axisX + ", 0)");

            const WIDTH_BAR = (WIDTH_SVG - axisX - MARGIN_PLOT_OUTSIDE * 2) / dataset.length - MARGIN_X_BAR;
            rectList
                .attr("x", function(d, i) {
                    var x = axisX + MARGIN_PLOT_OUTSIDE;
                    if (i == 0) {
                        return x;
                    }
                    x += i * (WIDTH_BAR + MARGIN_X_BAR);
                    return x;
                })
                .attr("width", function(d, i) {
                    return WIDTH_BAR;
                });
        </script>
    </body>
</html>

D3.jsでアニメーションさせる

インタラクティブな制御をしても、変化が一瞬だと面白くなく、アニメーションで変化して欲しいときがあります。
そういった場合にはメソッドチェーン中にtransitionメソッドを挟むと、それ以降の設定の変更が瞬時に切り替わるのではなくアニメーションするようになります。

                .on("click", function() {
                    d3.select(this)
                        .transition()
                        .attr("fill", "#cc3333");

また、アニメーション時間の設定はdurationメソッドをtransitionと属性などの変更との間に挟むことで設定します。単位はミリ秒で、例えば1000を指定したら1秒かけてアニメーションするようになります。デフォルトではうろ覚えですが、0.3秒?程度の短い時間が設定されます。

                .on("click", function() {
                    d3.select(this)
                        .transition()
                        .duration(1000)
                        .attr("fill", "#cc3333");

また、変更対象の設定値などは複数設定ができます。
たとえば、色だけではなく高さの値も同時にアニメーションさせたりもできます。

                .on("click", function() {
                    d3.select(this)
                        .transition()
                        .duration(1000)
                        .attr("fill", "#cc3333")
                        .attr("height", 0);
                });

GIFアニメで確認する

D3.jsで色の指定をRGBA個別に行う

fill属性では、#ffffffといった16進数による指定だけではなく、RGBの値を0~255、A(透明度)の値を0~1で個別に指定することができます。
値に文字列で"rgb(赤, 緑, 青)"といったように指定したり、"rgba(赤, 緑, 青, 透明度)"といったように指定することができます。

                .on("click", function() {
                    d3.select(this)
                        .transition()
                        .duration(1000)
                        .attr("fill", "rgba(0, 180, 255, 1)");
                });

16進数の指定じゃだめなのか?という点ですが、RGBを個別に整数で指定することで、例えばデータセットの値に応じて色を決定したりといった制御がやりやすくなります。アニメーション設定時などでもよく使ったりします。

**[クリックでコード全体を表示する]**
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script type="text/javascript" src="./d3.js"></script>
    </head>
    <body>
        <style>
            .axis path,
            .axis line {
                stroke: #aaaaaa;

                /* cripsEdgesを指定すると、ピクセル数が小数などの中途半端な
                値でぼやけて見栄えが悪くなるのを防止できる。*/
                shape-rendering: crispEdges;
            }

            .axis text {
                font-family: Arial, Helvetica, sans-serif;
                fill: #aaaaaa;
            }
        </style>
        <script type="text/javascript">

            const WIDTH_SVG = 500;
            const HEIGHT_SVG = 260;
            const MARGIN_X_BAR = 1;
            const MARGIN_PLOT_OUTSIDE = 10;

            var svg = d3.select("body")
                .append("svg")
                .attr("width", WIDTH_SVG)
                .attr("height", HEIGHT_SVG)
                .style("background-color", "#333333");

            var dataset = [
                150, 96, 59, 50, 73, 119, 175, 223, 248, 242,
                208, 155, 100, 61, 50, 69, 114, 170, 219, 247,
                244, 212, 160, 105, 64, 50, 66, 109, 164, 215];

            var heightScale = d3.scaleLinear()
                .domain([0, d3.max(dataset)])
                .range([0, HEIGHT_SVG - MARGIN_PLOT_OUTSIDE * 2])

            var rectList = svg.selectAll("rect")
                .data(dataset)
                .enter()
                .append("rect")
                .attr("height", function(d, i) {
                    return heightScale(d);
                })
                .attr("y", function(d) {
                    return HEIGHT_SVG - heightScale(d) - MARGIN_PLOT_OUTSIDE;
                })
                .attr("fill", "#dddddd")
                .on("click", function() {
                    d3.select(this)
                        .transition()
                        .duration(1000)
                        .attr("fill", "rgba(0, 180, 255, 1)");
                });

            var axisScale = d3.scaleLinear()
                .domain([0, d3.max(dataset)])
                .range([HEIGHT_SVG - MARGIN_PLOT_OUTSIDE, MARGIN_PLOT_OUTSIDE]);

            var yAxis = d3.axisLeft()
                .scale(axisScale)
                .ticks(5);

            var axisGroup = svg.append("g")
                .classed("axis", true)
                .call(yAxis);

            var axisBBox = axisGroup.node()
                .getBBox();

            var axisX = MARGIN_PLOT_OUTSIDE + axisBBox.width;
            axisGroup.attr("transform", "translate(" + axisX + ", 0)");

            const WIDTH_BAR = (WIDTH_SVG - axisX - MARGIN_PLOT_OUTSIDE * 2) / dataset.length - MARGIN_X_BAR;
            rectList
                .attr("x", function(d, i) {
                    var x = axisX + MARGIN_PLOT_OUTSIDE;
                    if (i == 0) {
                        return x;
                    }
                    x += i * (WIDTH_BAR + MARGIN_X_BAR);
                    return x;
                })
                .attr("width", function(d, i) {
                    return WIDTH_BAR;
                });
        </script>
    </body>
</html>

イージング設定

デフォルトでもイーズインアウト的な設定がされており、そのままでもそこまで違和感はありません。
デフォルト値以外を指定したい場合には、easeメソッドをdurationメソッドと同じような位置のメソッドチェーンに差し込むことで調整ができます。


                .on("click", function() {
                    d3.select(this)
                        .transition()
                        .duration(1000)
                        .ease(d3.easeQuadInOut)
                        .attr("fill", "rgba(0, 180, 255, 1)");
                });

※各イージング関数がどんな感じなのかは以下のサイトで実際にアニメーションを見ながら確認できます。
D3.js v4/v5 アニメ―ション使い方 エフェクト一覧(transition, ease)

アニメーションが終わったら次のアニメーションを開始する

メソッドチェーン中の、変更する属性などの後に別のtransitionメソッドを追加することで、1つ目のアニメーションが終わったら、2つ目のアニメーションを開始する、といった制御が可能になります。

色変更のアニメーションを設定して、終わったら棒の高さを変更するアニメーションを実行する例 :

                .on("click", function() {
                    d3.select(this)
                        .transition()
                        .duration(1000)
                        .ease(d3.easeQuadInOut)
                        .attr("fill", "rgba(0, 180, 255, 1)")
                        .transition()
                        .duration(1000)
                        .attr("height", 0);
                })

GIFアニメで確認する

アニメーションの開始を遅延させる

例えば、1つ目のアニメーションが終わってから2つ目のアニメーションを開始する際に、1秒待ってから開始するといった指定をしたい場合にはdelayメソッドをdurationなどと同じような位置に挟みます。

               .on("click", function() {
                    d3.select(this)
                        .transition()
                        .duration(1000)
                        .ease(d3.easeQuadInOut)
                        .attr("fill", "rgba(0, 180, 255, 1)")
                        .transition()
                        .duration(1000)
                        .delay(2000)
                        .attr("height", 0);
                })

GIFアニメで確認する

D3.jsのスケールオブジェクトに対してアニメーションを設定する

たとえば、ユーザーのインタラクティブな操作で要素を追加したり削除したりした際に、スケールの設定が不適切になるケースが発生します。(一番大きい値の棒が非表示になり、表示領域で空いているスペースが大きくできてしまうなど)
もしくは一部分にフォーカスするため、軸の範囲を狭めるなど。
そういった場合には、生成したスケールオブジェクトのdomainの値を更新して、棒要素や軸などにtransitionと共に反映することで更新することができます。
SVGの表示領域のサイズは基本変わらないことが多いと思うので、rangeメソッド側はそのままで、domainメソッド側のみ更新します。
軸に関しては、もう一度callメソッドでaxisLeftなどで作ったオブジェクトを実行するだけで対応してくれます。

                .on("click", function() {

                    // データの表示範囲を0~100に指定する。100以上のデータ
                    // も存在するので、プロット上で見切れて表示される。
                    heightScale.domain([0, 100]);
                    axisScale.domain([0, 100]);

                    // 棒グラフに調整後のスケールオブジェクトの値を
                    // アニメーションさせながら反映する。
                    d3.selectAll("rect")
                        .transition()
                        .duration(1000)
                        .attr("height", function(d, i) {
                            return heightScale(d);
                        })
                        .attr("y", function(d) {
                            return HEIGHT_SVG - heightScale(d) - MARGIN_PLOT_OUTSIDE;
                        });

                    // 軸の値も合わせて更新する必要があるのでこちらも
                    // アニメーション設定を行う。
                    axisGroup
                        .transition()
                        .duration(1000)
                        .call(yAxis);
                })

GIFアニメで確認する

**[クリックでコード全体を表示する]**
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script type="text/javascript" src="./d3.js"></script>
    </head>
    <body>
        <style>
            .axis path,
            .axis line {
                stroke: #aaaaaa;

                /* cripsEdgesを指定すると、ピクセル数が小数などの中途半端な
                値でぼやけて見栄えが悪くなるのを防止できる。*/
                shape-rendering: crispEdges;
            }

            .axis text {
                font-family: Arial, Helvetica, sans-serif;
                fill: #aaaaaa;
            }
        </style>
        <script type="text/javascript">

            const WIDTH_SVG = 500;
            const HEIGHT_SVG = 260;
            const MARGIN_X_BAR = 1;
            const MARGIN_PLOT_OUTSIDE = 10;

            var svg = d3.select("body")
                .append("svg")
                .attr("width", WIDTH_SVG)
                .attr("height", HEIGHT_SVG)
                .style("background-color", "#333333");

            var dataset = [
                150, 96, 59, 50, 73, 119, 175, 223, 248, 242,
                208, 155, 100, 61, 50, 69, 114, 170, 219, 247,
                244, 212, 160, 105, 64, 50, 66, 109, 164, 215];

            var heightScale = d3.scaleLinear()
                .domain([0, d3.max(dataset)])
                .range([0, HEIGHT_SVG - MARGIN_PLOT_OUTSIDE * 2]);

            var axisScale = d3.scaleLinear()
                .domain([0, d3.max(dataset)])
                .range([HEIGHT_SVG - MARGIN_PLOT_OUTSIDE, MARGIN_PLOT_OUTSIDE]);

            var rectList = svg.selectAll("rect")
                .data(dataset)
                .enter()
                .append("rect")
                .attr("height", function(d, i) {
                    return heightScale(d);
                })
                .attr("y", function(d) {
                    return HEIGHT_SVG - heightScale(d) - MARGIN_PLOT_OUTSIDE;
                })
                .attr("fill", "#dddddd")
                .on("click", function() {

                    // データの表示範囲を0~100に指定する。100以上のデータ
                    // も存在するので、プロット上で見切れて表示される。
                    heightScale.domain([0, 100]);
                    axisScale.domain([0, 100]);

                    // 棒グラフに調整後のスケールオブジェクトの値を
                    // アニメーションさせながら反映する。
                    d3.selectAll("rect")
                        .transition()
                        .duration(1000)
                        .attr("height", function(d, i) {
                            return heightScale(d);
                        })
                        .attr("y", function(d) {
                            return HEIGHT_SVG - heightScale(d) - MARGIN_PLOT_OUTSIDE;
                        });

                    // 軸の値も合わせて更新する必要があるのでこちらも
                    // アニメーション設定を行う。
                    axisGroup
                        .transition()
                        .duration(1000)
                        .call(yAxis);
                });

            var yAxis = d3.axisLeft()
                .scale(axisScale)
                .ticks(5);

            var axisGroup = svg.append("g")
                .classed("axis", true)
                .call(yAxis);

            var axisBBox = axisGroup.node()
                .getBBox();

            var axisX = MARGIN_PLOT_OUTSIDE + axisBBox.width;
            axisGroup.attr("transform", "translate(" + axisX + ", 0)");

            const WIDTH_BAR = (WIDTH_SVG - axisX - MARGIN_PLOT_OUTSIDE * 2) / dataset.length - MARGIN_X_BAR;
            rectList
                .attr("x", function(d, i) {
                    var x = axisX + MARGIN_PLOT_OUTSIDE;
                    if (i == 0) {
                        return x;
                    }
                    x += i * (WIDTH_BAR + MARGIN_X_BAR);
                    return x;
                })
                .attr("width", function(d, i) {
                    return WIDTH_BAR;
                });
        </script>
    </body>
</html>

他にも色々ありますが、燃え尽きたので今回はこの辺にしておきます・・
いいねが少しは付いたら、追記を考えます・・!

まとめ

SVG・D3.js楽しい!
これで自分オリジナルのプロットライブラリとか作れるよ。やったね!

参考

89
85
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
89
85

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?