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
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 {
}
},
}
});