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

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

#### サンプル001■ Vue.js + ES6: Radar chart with SVG controlled dynamically - final
<p class="codepen" data-height="480" data-theme-id="default" data-default-tab="js,result" data-user="FumioNonaka" data-slug-hash="yLLdJJp" style="height: 480px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Vue.js + ES6: Radar chart with SVG controlled dynamically - final">
  <span>See the Pen <a href="https://codepen.io/FumioNonaka/pen/yLLdJJp">
  Vue.js + ES6: Radar chart with SVG controlled dynamically - final</a> by Fumio Nonaka (<a href="https://codepen.io/FumioNonaka">@FumioNonaka</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>


#ECMAScript 2015の構文に書き替える
公式作例は、つぎのようなECMAScript 5の構文で書かれています。

```js
var stats = [

]

Vue.component('polygraph', {

	computed: {
		points: function () {

		}
	},

});
```

ECMAScript 2015では、変数は基本的に[`const`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/const)または[`let`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/let)で宣言します。また、オブジェクトにメソッドを定めるとき、コロン(`:`)と`function`キーワードが省けます(「[ECMAScript 2015での新しい表記法](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Object_initializer#ECMAScript_2015_%E3%81%A7%E3%81%AE%E6%96%B0%E3%81%97%E3%81%84%E8%A1%A8%E8%A8%98%E6%B3%95)」)。

```js
const stats = [

];

Vue.component('polygraph', {

	computed: {
		points() {

		}
	},

});
```

#SVGの円のデータとコードを関連づける
公式作例でレーダーチャートの頂点座標を求める関数には、つぎのような直打ちされた数値があります。

```js
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ピクセルということです。

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

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

</g>
</script>
```

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

```js
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`ディレクティブ](https://jp.vuejs.org/v2/api/#v-bind)(省略記法`:`)でオブジェクトをバインディングして用います。

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

</div>
</body>
```
```html
<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')$はつぎの式で求められます(「[任意の座標を原点から指定した角度回す](https://qiita.com/FumioNonaka/items/c246aca8f1b1b03a66be)」参照)。座標の原点からの距離を求めなくて済み、多くの点を同じ角度回す場合は便利です。

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

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

```js
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$から座標を求めるつぎの式の方が端的でしょう。

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

####三角関数のsinとcos
![図001](https://qiita-image-store.s3.amazonaws.com/0/128949/8bdaad21-321b-9a8d-3914-8b4f5b2ee7f3.png)

```js
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「[スタイルガイド](https://jp.vuejs.org/v2/style-guide/)」で「優先度A: 必須」の修正も加えます。まず、コンポーネントの`props`です。

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

});
```

プロパティには、少なくともタイプを定めるべきです(「[プロパティの定義](https://jp.vuejs.org/v2/style-guide/#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%AE%E5%AE%9A%E7%BE%A9-%E5%BF%85%E9%A0%88)」参照)。

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

	},

});
```

つぎに、[`v-for`ディレクティブ](https://jp.vuejs.org/v2/guide/list.html#v-for-%E3%81%A7%E9%85%8D%E5%88%97%E3%81%AB%E8%A6%81%E7%B4%A0%E3%82%92%E3%83%9E%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0%E3%81%99%E3%82%8B)には、[`key`属性](https://jp.vuejs.org/v2/guide/list.html#key)を与えなければなりません(「[キー付きv-for](https://jp.vuejs.org/v2/style-guide/#%E3%82%AD%E3%83%BC%E4%BB%98%E3%81%8D-v-for-%E5%BF%85%E9%A0%88)」参照)。`key`がないと、開発用のVue.jsはコンソールにつぎのような警告を示します。

> [Vue tip]: <axis-label v-for="stat in stats">: component lists rendered with v-for should have explicit keys. See https://vuejs.org/guide/list.html#key for more info.

そこで、レーダーチャート用のデータに、一意のプロパティを加えます。[`Array.prototype.map()`メソッド](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/map)でインデックスの値を与えることで、手打ちの手間は省きました。

```js
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`にプロパティ値を定めます。

```html
<body>
<div id="demo">

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

	</div>

</div>
</body>
```
```html
<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から備わった[スプレッド構文`...`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Spread_syntax)は、配列をカンマ(`,`)区切りの引数として渡します。

```js
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`](https://jp.vuejs.org/v2/api/#el)でアプリケーションとする要素を定めています。

```js
new Vue({
	el: '#demo',

}) 
```

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

```js
const vm = new Vue({

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

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

```js
new Vue({

	methods: {
		add: function (e) {

			if (!this.newLabel) return

			this.newLabel = ''
		},

	}
})
```

そこで、[`String.prototype.trim()`メソッド](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim)を用いて、スペースだけの場合も弾くこととしました。

```js
const vm = new Vue({

	methods: {
		add(event) {

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

		},

	}
});
```

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

```js
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()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)を使うことにしました。引数の関数に定めたのは、削除する要素以外という条件です。これで、その要素の除かれた配列に改められます。

```js
const vm = new Vue({

	methods: {

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

			}
		},

	}
});
```