Help us understand the problem. What is going on with this article?

Vue.js公式サイトの「SVGグラフの例」を改善する

Vue.js公式サイトの「SVGグラフの例」は、SVGでレーダーチャートを描き、値がスライダで動的に操作できます。さらに、データを新たに加えたり除いたりすることも可能です。この作例をECMAScript 2015 (ECMAScript 6)の構文に改めたうえ、コードの手直しも加えてみました(サンプル001)。その改善の要点をかいつまんでご説明します。コードの詳しい解説は「Vue.js + ES6: SVGでレーダーチャートを操作する」をお読みください。

サンプル001■ Vue.js + ES6: Radar chart with SVG controlled dynamically - final

See the Pen Vue.js + ES6: Radar chart with SVG controlled dynamically - final by Fumio Nonaka (@FumioNonaka) on CodePen.

ECMAScript 2015の構文に書き替える

公式作例は、つぎのようなECMAScript 5の構文で書かれています。

var stats = [

]

Vue.component('polygraph', {

    computed: {
        points: function () {

        }
    },

});

ECMAScript 2015では、変数は基本的にconstまたはletで宣言します。また、オブジェクトにメソッドを定めるとき、コロン(:)とfunctionキーワードが省けます(「ECMAScript 2015での新しい表記法」)。

const stats = [

];

Vue.component('polygraph', {

    computed: {
        points() {

        }
    },

});

SVGの円のデータとコードを関連づける

公式作例でレーダーチャートの頂点座標を求める関数には、つぎのような直打ちされた数値があります。

function valueToPoint (value, index, total) {

    var y = -value * 0.8

    var tx = x * cos - y * sin + 100
    var ty = x * sin + y * cos + 100

}

この数値は、SVGの<circle>要素の属性値からきています。円の中心座標が(100, 100)、半径はチャートの値が100%のとき80ピクセルということです。

<script type="text/x-template" id="polygraph-template">
<g>

    <circle cx="100" cy="100" r="80"></circle>

</g>
</script>

そこで、これら<circle>要素の属性値はオブジェクトのプロパティにまとめ、JavaScriptコードはその値を参照することにします。

const circle = {cx: 100, cy: 100, r: 80};

function valueToPoint(value, index, total) {

    const y = -value * circle.r / 100;

    const tx = x * cos - y * sin + circle.cx;
    const ty = x * sin + y * cos + circle.cy;

}

<body>要素とテンプレートは、v-bindディレクティブ(省略記法:)でオブジェクトをバインディングして用います。

<body>
<div id="demo">
    <svg width="200" height="200">
        <polygraph :stats="stats" :circle="circle"></polygraph>
    </svg>

</div>
</body>
<script type="text/x-template" id="polygraph-template">
<g>

    <circle :cx="circle.cx" :cy="circle.cy" :r="circle.r"></circle>

</g>
</script>

原点からの距離と角度により座標を求める

任意の点$(x, y)$を、原点$(0, 0)$から角度$\theta$回した座標$(x', y')$はつぎの式で求められます(「任意の座標を原点から指定した角度回す」参照)。座標の原点からの距離を求めなくて済み、多くの点を同じ角度回す場合は便利です。

x' = x\cos\theta - y\sin\theta\\
y' = x\sin\theta + y\cos\theta

公式作例では、レーダーチャートの頂点座標をこの式で導いています。

function valueToPoint (value, index, total) {
    var x = 0
    var y = -value * 0.8
    var angle = Math.PI * 2 / total * index
    var cos = Math.cos(angle)
    var sin = Math.sin(angle)
    var tx = x * cos - y * sin + 100
    var ty = x * sin + y * cos + 100
    return {
        x: tx,
        y: ty
    }
}

けれど、この作例では頂点座標の原点からの距離が予め(パーセンテージで)与えられており、角度は頂点ごとに異なります。この場合、距離$r$と角度$\theta$から座標を求めるつぎの式の方が端的でしょう。

x = r\cos\theta\\
y = r\sin\theta

三角関数のsinとcos

図001

function valueToPoint(value, index, total) {
    const r = value * circle.r / 100;
    const angle = Math.PI * 2 / total * index - Math.PI / 2;
    const tx = r * Math.cos(angle) + circle.cx;
    const ty = r * Math.sin(angle) + circle.cy;
    return {
        x: tx,
        y: ty
    };
}

Vue.js「スタイルガイド」に合わせる

Vue.js「スタイルガイド」で「優先度A: 必須」の修正も加えます。まず、コンポーネントのpropsです。

Vue.component('polygraph', {
    props: ['stats'],

});

プロパティには、少なくともタイプを定めるべきです(「プロパティの定義」参照)。

Vue.component('polygraph', {
    props: {
        stats: Array,

    },

});

つぎに、v-forディレクティブには、key属性を与えなければなりません(「キー付きv-for」参照)。keyがないと、開発用のVue.jsはコンソールにつぎのような警告を示します。

[Vue tip]: : component lists rendered with v-for should have explicit keys. See https://vuejs.org/guide/list.html#key for more info.

そこで、レーダーチャート用のデータに、一意のプロパティを加えます。Array.prototype.map()メソッドでインデックスの値を与えることで、手打ちの手間は省きました。

const stats = [
    {label: 'A', value: 100},
    {label: 'B', value: 100},
    {label: 'C', value: 100},
    {label: 'D', value: 100},
    {label: 'E', value: 100},
    {label: 'F', value: 100}
].map((stat, index) => {
    stat.id = index;
    return stat;
});

そして、v-forディレクティブが用いられた要素には、keyにプロパティ値を定めます。

<body>
<div id="demo">

    <div v-for="stat in stats"
        :key="stat.id">

    </div>

</div>
</body>
<script type="text/x-template" id="polygraph-template">
<g>

    <axis-label
        v-for="(stat, index) in stats"

        :key="stat.id">
    </axis-label>
</g>
</script>

データを加えるメソッドも、一意の数値をプロパティとして加えなければなりません。Array.prototype.map()メソッドですでに使われている値を取り出し、Math.max()メソッドにより得た最大値に1を加えたのが新たな値です。ECMAScript 2015から備わったスプレッド構文...は、配列をカンマ(,)区切りの引数として渡します。

const vm = new Vue({

    methods: {
        add(event) {

            this.stats.push({

                id: this.getNewId()
            })
        },

        getNewId() {
            const ids = this.stats.map((stat) => stat.id);
            return Math.max(...ids) + 1;
        }
    }
});

その他の修正

ほかにも、細かい修正を3つほど加えました。第1に、公式作例はVue()コンストラクタのオプションオブジェクトにelでアプリケーションとする要素を定めています。

new Vue({
    el: '#demo',

}) 

このelは省いて、DOMContentLoadedイベントのリスナー関数からvm.$mount()メソッドを呼び出すようにしました。これで、JavaScriptコードは<head>要素に書けます。

const vm = new Vue({

});
document.addEventListener('DOMContentLoaded', (event) =>
    vm.$mount('#demo')
);

第2に、公式作例は新たなデータを加えるとき、テキストが空かどうかしか確かめていません。これですと、スペースだけでも通ってしまいます。

new Vue({

    methods: {
        add: function (e) {

            if (!this.newLabel) return

            this.newLabel = ''
        },

    }
})

そこで、String.prototype.trim()メソッドを用いて、スペースだけの場合も弾くこととしました。

const vm = new Vue({

    methods: {
        add(event) {

            const newLabel = this.newLabel.trim();
            this.newLabel = '';
            if (!newLabel) {return}

        },

    }
});

第3は、公式作例がデータの削除にArray.prototype.indexOf()メソッドを使っていることです。このメソッドを用いるとき気をつけなければならないのは、参照する配列そのものが変わってしまうことです。配列要素のループ処理で用いると、取り出す順序が壊されるおそれもあります。

new Vue({

    methods: {

        remove: function (stat) {
            if (this.stats.length > 3) {
                this.stats.splice(this.stats.indexOf(stat), 1)
            } else {

            }
        }
    }
})

もっとも、この例では配列そのものからデータを除きたいわけですし、ループ処理でもありません。けれど、ECMAScript 5.1の新しいメソッドとしてArray.prototype.filter()を使うことにしました。引数の関数に定めたのは、削除する要素以外という条件です。これで、その要素の除かれた配列に改められます。

const vm = new Vue({

    methods: {

        remove(stat) {
            const stats = this.stats;
            if (stats.length > 3) {
                this.stats = stats.filter((_stat) => _stat !== stat);
            } else {

            }
        },

    }
});
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした