SVGで数をモリモリ数える

# SVGで数をモリモリ数える

珍しくデータビジュアライズ系の仕事がありまして、初めてSVGを触りました。
その中にSVGで数字を数える部分があり、その部分を分離して皆さんの環境でスグに試せるようにES5のJavaScriptで書き直したのでご紹介します。

## デモ
http://codepen.io/makipe/pen/NRpXdR

## ソース全体
### index.html
```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
    html {
        background-color: #000;
    }
    </style>
    <script type="text/javascript" src="./app.js"></script>
</head>
<body>
    <main id="app"></main>
</body>
</html>
```

### app.js
```js:app.js
(function() {
    'use strict';

    var number = {};
    number.settings = {
        color: {
            on: '#32FFFF',
            off: '#000000'
        },
        min: 0,
        max: 9
    };

    number.patterns = [
        [1,1,1,1,0,1,1], // 0
        [1,1,0,0,0,0,0], // 1
        [0,1,1,0,1,1,1], // 2
        [1,1,1,0,1,1,0], // 3
        [1,1,0,1,1,0,0], // 4
        [1,0,1,1,1,1,0], // 5
        [1,0,1,1,1,1,1], // 6
        [1,1,1,0,0,0,0], // 7
        [1,1,1,1,1,1,1], // 8
        [1,1,1,1,1,1,0], // 9
    ];

    number.base_path_list = [
        'M60.189,64.438c1.841,1.157,3.129,3.11,3.388,5.383l-3.153,36.068c-0.339,3.813-2.863,7.133-6.234,8.727l-5.121-11.359 c0.142-0.25,0.234-0.524,0.263-0.813l2.871-32.833L60.189,64.438z',
        'M57.396,8.304h1.621c5.371,0,9.383,4.375,8.918,9.719l-3.312,37.822c-0.586,2.382-2.336,4.304-4.61,5.132l-6.695-6.117 L57.396,8.304z',
        'M54.459,19.804c-0.187-0.062-0.391-0.102-0.598-0.102H26.662c-0.703,0-1.351,0.399-1.707,0.977l-10.262-6.398 c1.793-3.477,5.485-5.922,9.5-5.922h31.266L54.459,19.804z',
        'M13.928,61.102c-2.27-1.18-3.82-3.469-3.953-6.133l3.187-36.439c0.09-0.922,0.297-1.82,0.61-2.664l10.454,6.508 l-2.97,33.923h0.156L13.928,61.102z',
        'M58.893,62.86 51.405,67.711 22.143,67.711 15.819,62.29 24.483,56.727 52.182,56.727',
        'M52.201,115.499c-1.03,0.344-2.125,0.531-3.241,0.531H14.838c-3.891,0-7.071-2.297-8.348-5.602l11.34-6.579 c0.281,0.5,0.824,0.837,1.465,0.837h27.198c0.263,0,0.523-0.056,0.759-0.164L52.201,115.499z',
        'M5.838,108.475c-0.209-0.922-0.287-1.899-0.199-2.907l3.082-35.239c0.047-3.288,2.211-6.07,5.199-6.992l6.332,5.431 l-2.891,33.012L5.838,108.475z'
    ];

    number.svg_list = [];
    number.path_list = [];

    number.create = function(number_digits) {
        var digits = number_digits === undefined ? 1 : number_digits;

        for (var i = 0; i < digits; i++) {
            var ns = 'http://www.w3.org/2000/svg';
            var svg = document.createElementNS(ns, 'svg');
            svg.setAttribute('viewBox', '0 0 76 126');
            svg.setAttribute('enable-background', '0 0 76 126');
            svg.setAttribute('x', 0);
            svg.setAttribute('y', 0);
            svg.setAttribute('width', 76);
            svg.setAttribute('height', 126);

            for (var j in number.base_path_list) {
                var path = document.createElementNS(ns, 'path');
                path.setAttribute('d', number.base_path_list[j]);
                svg.appendChild(path);
            }

            var app = document.getElementById('app');
            app.appendChild(svg);
            number.svg_list.push(svg);
        }
    };

    number.init = function() {
        var svg_list = number.svg_list;
        var svg_length = svg_list.length;
        for (var i = 0; i < svg_length; i++) {
            number.path_list[i] = svg_list[i].querySelectorAll('path');
            var path_length = number.path_list[i].length;
            for (var j = 0; j < path_length; j++) {
                number.path_list[i][j].setAttribute('fill', number.settings.color.off);
            }
        }
    };

    number.getDigits = function(num) {
        var digits = (num + '').split('');
        var digit_diff = 0;
        if (number.svg_list.length > digits.length) {
            digit_diff = number.svg_list.length - digits.length;
            var zero_padding = Array(digit_diff + 1).join('0');
            digits = (zero_padding + num).split('');
        }
        return digits;
    };

    number.update = function(digits) {
        digits.map(function(digit, i) {
            if (number.path_list.length > digits.length) {
                i += 1;
            }
            var path_list = number.path_list[i];
            var length = path_list.length;
            for (var i = 0; i < length; i++) {
                if (number.patterns[digit][i] === 1) {
                    path_list[i].setAttribute('fill', number.settings.color.on);
                } else {
                    path_list[i].setAttribute('fill', number.settings.color.off);
                }
            }
        });
    };

    document.addEventListener('DOMContentLoaded', function() {
        number.create(8);
        number.init();

        var number_digits = number.svg_list.length;
        number.settings.max = Array(number_digits + 1).join(number.settings.max + '');

        var count = number.settings.min;
        var timer = setInterval(function() {
            if (count > number.settings.max) {
                count = number.settings.min;
            }
            var digits = number.getDigits(count);
            number.update(digits);
            count++;
        }, 1000 / (number_digits * 10));
    });
})();

```
## app.js 各部の説明

### 設定値
数字の各パーツの有効／無効の色と、表示する数字の最大／最小値を設定しています。

```js
number.settings = {
    color: {
        on: '#32FFFF',
        off: '#000000'
    },
    min: 0,
    max: 9
};
```

### 数字の表示パターン
この先の処理で各path要素をリストアップして配列にしています。
そのpath要素の配列に対して、各数字を表示する時に有効にしたいpath要素の位置にフラグを立てています。

```js
number.patterns = [
    [1,1,1,1,0,1,1], // 0
    [1,1,0,0,0,0,0], // 1
    [0,1,1,0,1,1,1], // 2
    [1,1,1,0,1,1,0], // 3
    [1,1,0,1,1,0,0], // 4
    [1,0,1,1,1,1,0], // 5
    [1,0,1,1,1,1,1], // 6
    [1,1,1,0,0,0,0], // 7
    [1,1,1,1,1,1,1], // 8
    [1,1,1,1,1,1,0], // 9
];
```

### 数字のパス
IllustratorでSVGとして書き出してパスを取得しました。
今回はパスの値をループで回して処理したいのでまとめて配列に入れておきました。

```js
number.base_path_list = [
    'M60.189,64.438c1.841,1.157,3.129,3.11,3.388,5.383l-3.153,36.068c-0.339,3.813-2.863,7.133-6.234,8.727l-5.121-11.359 c0.142-0.25,0.234-0.524,0.263-0.813l2.871-32.833L60.189,64.438z',
    'M57.396,8.304h1.621c5.371,0,9.383,4.375,8.918,9.719l-3.312,37.822c-0.586,2.382-2.336,4.304-4.61,5.132l-6.695-6.117 L57.396,8.304z',
    'M54.459,19.804c-0.187-0.062-0.391-0.102-0.598-0.102H26.662c-0.703,0-1.351,0.399-1.707,0.977l-10.262-6.398 c1.793-3.477,5.485-5.922,9.5-5.922h31.266L54.459,19.804z',
    'M13.928,61.102c-2.27-1.18-3.82-3.469-3.953-6.133l3.187-36.439c0.09-0.922,0.297-1.82,0.61-2.664l10.454,6.508 l-2.97,33.923h0.156L13.928,61.102z',
    'M58.893,62.86 51.405,67.711 22.143,67.711 15.819,62.29 24.483,56.727 52.182,56.727',
    'M52.201,115.499c-1.03,0.344-2.125,0.531-3.241,0.531H14.838c-3.891,0-7.071-2.297-8.348-5.602l11.34-6.579 c0.281,0.5,0.824,0.837,1.465,0.837h27.198c0.263,0,0.523-0.056,0.759-0.164L52.201,115.499z',
    'M5.838,108.475c-0.209-0.922-0.287-1.899-0.199-2.907l3.082-35.239c0.047-3.288,2.211-6.07,5.199-6.992l6.332,5.431 l-2.891,33.012L5.838,108.475z'
];
```

### SVGでできた数字を画面に追加する

document.createElementNS()を使用します。
第一引数はSVGの名前空間のURL、第二引数は作成したい要素名を入れます。
（document.createElement()を使用すると要素はあるけど表示されない状態になってしまいます。）

そして、先ほど用意したパスの配列をくるくる回してpath要素を作ります。
この時、d属性にパスの値を指定します。
svg要素にpath要素を追加できたら、HTML上の任意の要素にsvg要素を追加します。

```js
number.create = function(number_digits) {
    var digits = number_digits === undefined ? 1 : number_digits;

    for (var i = 0; i < digits; i++) {
        var ns = 'http://www.w3.org/2000/svg';
        var svg = document.createElementNS(ns, 'svg');
        svg.setAttribute('viewBox', '0 0 76 126');
        svg.setAttribute('enable-background', '0 0 76 126');
        svg.setAttribute('x', 0);
        svg.setAttribute('y', 0);
        svg.setAttribute('width', 76);
        svg.setAttribute('height', 126);

        for (var j in number.base_path_list) {
            var path = document.createElementNS(ns, 'path');
            path.setAttribute('d', number.base_path_list[j]);
            svg.appendChild(path);
        }

        var app = document.getElementById('app');
        app.appendChild(svg);
        number.svg_list.push(svg);
    }
};
```

### path要素を初期化する
svg要素の数だけ、path要素をセレクトして設定値の無効時の色を適応していきます。

```js
number.init = function() {
    var svg_list = number.svg_list;
    var svg_length = svg_list.length;
    for (var i = 0; i < svg_length; i++) {
        number.path_list[i] = svg_list[i].querySelectorAll('path');
        var path_length = number.path_list[i].length;
        for (var j = 0; j < path_length; j++) {
            number.path_list[i][j].setAttribute('fill', number.settings.color.off);
        }
    }
};
```

### カウントする数字を１桁ごとに配列に入れる
各桁の数字ごとに配列の値に変換したデータを取得します。
また、svg要素の数とカウントしている数字の桁数が合うように調整しています。

```js
number.getDigits = function(num) {
    var digits = (num + '').split('');
    var digit_diff = 0;
    if (number.svg_list.length > digits.length) {
        digit_diff = number.svg_list.length - digits.length;
        var zero_padding = Array(digit_diff + 1).join('0');
        digits = (zero_padding + num).split('');
    }
    return digits;
};
```

例1） svg要素が3個でカウントしている数字が「123」の場合 [1,2,3]という配列になる
例2） svg要素が2個でカウントしている数字が「4」の場合 [0,4]という配列になる

### 桁毎の数字が入った配列を受け取り描画を更新する
桁毎の数字が入った配列と数字の表示パターンのデータを利用して、
各path要素に色を付けるか判定していきます。

```js
number.update = function(digits) {
    digits.map(function(digit, i) {
        if (number.path_list.length > digits.length) {
            i += 1;
        }
        var path_list = number.path_list[i];
        var length = path_list.length;
        for (var i = 0; i < length; i++) {
            if (number.patterns[digit][i] === 1) {
                path_list[i].setAttribute('fill', number.settings.color.on);
            } else {
                path_list[i].setAttribute('fill', number.settings.color.off);
            }
        }
    });
};
```

### 呼び出し
DOMの解析が終わったら好みに合わせそれぞれ実行していきます。
svg要素の生成部分は任意の桁数分生成できるようになっています。
また、そのｓｖｇ要素の桁数を最大値に自動的に設定し直しています。

```js
document.addEventListener('DOMContentLoaded', function() {
    number.create(8);
    number.init();

    var number_digits = number.svg_list.length;
    number.settings.max = Array(number_digits + 1).join(number.settings.max + '');

    var count = number.settings.min;
    var timer = setInterval(function() {
        if (count > number.settings.max) {
            count = number.settings.min;
        }
        var digits = number.getDigits(count);
        number.update(digits);
        count++;
    }, 1000 / (number_digits * 10));
});
```

## まとめ
svg要素の作成から、その内部要素の作成と属性の動的変更のやり方がわかりました。
これが分かれば後はドキュメントを見ながら様々なデータ表示を作成することができると思います。
今回、数字のカウンター以外に折れ線グラフや棒グラフ、メーターなどデータの変化に応じて動的に描画を変化させるものを他にも作成したので、機会があればご紹介できればと思います。

## 参考
+ [JavaScriptでHTMLのSVG要素（タグ）を生成する時はElementNS – JavaScript Archives（ジャバスクリプト アーカイブス）](http://www.takaya-com.jp/archives/2015/10/avg_createelementns/)
+ [Document.createElementNS() - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/Document/createElementNS)
+ [svg要素の基本的な使い方まとめ](http://www.h2.dion.ne.jp/~defghi/svgMemo/svgMemo_15.htm)
+ [SVG 属性リファレンス - SVG | MDN](https://developer.mozilla.org/ja/docs/Web/SVG/Attribute)
+ [SVG 1.1 仕様 （第２版） 日本語訳](https://triple-underscore.github.io/SVG11/index.html)