JavaScript
AngularJS
Angular2
Angular 2Day 15

Angular2でもとりあえずSVGを描いてみる

More than 3 years have passed since last update.


はじめに

私は日頃からSVGを使ったデータビジュアライゼーションをやっています。

2013年頃、はじめはSVG要素の構築にD3.jsを使っていましたが、性能と書きやすさ、保守性などのバランスをとって、最近ではAngularJSやReactへとライブラリを移行してきました。

AngularJSはお気に入りのライブラリですがデータビジュアライゼーションに使うという点では性能に不安がありました。

Angular2がリリースに向けて着々と準備されているということで、性能も改善されたというAngular2でSVGを描いてみます。

Angular1とAngular2で同じ機能を実装して違いを比べてみます。

どちらもTypeScriptで実装します。

Angular1のバージョンは1.4.8、Angular2のバージョンは2.0.0-alpha.53を使います。


Angular1による実装

まずはお決まりのやつから。

const app = angular.module('app', []);

本体の実装に入っていきますが、ComponentベースのAngular2と似せた実装をするためにAngular1でもdirectiveを使った実装をします。

今回はComponent1とComponent2という2つの独立したComponentから構成されるようにします。

まずはComponent1の実装です。

const component1Template = `

<div>
<h2>Component 1</h2>
<p>{{message}}</p>
<input ng-model="c1.message"/>
</div>
`
;

class Component1 {
message = 'hello';
}

app.directive('component1', () => ({
template: component1Template,
controllerAs: 'c1',
controller: Component1
}));

messageng-modelで双方向バインディングしています。

次に、Component2の実装です。

class Point {

x: number;
y: number;
}

const component2Template = `
<div>
<h2>Component 2</h2>
<div>
<input type="number" min="1" max="10" step="1" ng-model="c2.r"/>
</div>
<div>
<svg ng-attr-width="{{c2.size}}" ng-attr-height="{{c2.size}}">
<circle ng-attr-r="{{c2.r}}" ng-attr-cx="{{c2.cx(point)}}" ng-attr-cy="{{c2.cy(point)}}" ng-repeat="point in c2.points"/>
</svg>
</div>
</div>
`
;

class Component2 {
r = 5;
size = 500;
points: Point[] = [];

constructor() {
var n = 10;
for (var i = 0; i < n; ++i) {
this.points.push({
x: Math.random() * this.size,
y: Math.random() * this.size
});
}
}

cx(point: Point) : number {
return point.x; // 何かすごい計算をする
}

cy(point: Point) : number {
return point.y; // 何かすごい計算をする
}
}

app.directive('component2', () => ({
template: component2Template,
controllerAs: 'c2',
controller: Component2
}));

10個の円をSVGに描画しています。

constructorで円の座標をランダムに生成して、circle要素のcxcyへとデータバインドしています。

cxcyは、座標変換の処理を挟むイメージでControllerのメソッドを経由させています。

ただし説明の単純化のため、この例では初期座標をそのまま返しています。

場合によってはメソッドの中で複雑な計算をすることもあると思ってください。

また、円の半径を表すrはinput要素で変更可能です。

次に、Component1とComponent2の親ComponentになるAppです。

const appTemplate = `

<div>
<component1></component1>
<component2></component2>
</div>
`
;

class App {
}

app.directive('myApp', () => ({
template: appTemplate,
controllerAs: 'app',
controller: App
}));


Angular2による実装

Angular1版と同じ機能をAngular 2で実装していきます。

はじめにモジュールのimportです。

import {Component, Input, ChangeDetectionStrategy} from 'angular2/core'

import {CORE_DIRECTIVES} from 'angular2/common'
import {bootstrap} from 'angular2/platform/browser'

alpha.53からangular2/angular2からのimportが廃止されて、angular2/coreangular2/commonなどから個別にimportするように変更されました。

どこに何があるかは https://angular.io/docs/ts/latest/api/ などを見るといいでしょう。

Component1の実装は以下の通りです。

const component1Template = `

<div>
<h2>Component 1</h2>
<p>{{message}}</p>
<input [(ngModel)]="message"/>
</div>
`
;

@Component({
selector: 'component1',
template: component1Template
})
class Component1 {
message = 'hello';
}

[(ngModel)]="message"という記述で双方向データバインドを行っています。

Component2の実装は以下の通りです。

class Point {

x: number;
y: number;
}

const component2Template = `
<div>
<h2>Component 2</h2>
<div>
<input type="number" min="1" max="10" step="1" [(ngModel)]="r"/>
</div>
<div>
<svg [attr.width]="size" [attr.height]="size">
<circle [attr.r]="r" [attr.cx]="cx(point)" [attr.cy]="cy(point)" *ngFor="#point of points"/>
</svg>
</div>
</div>
`
;

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'component2',
template: component2Template
})
class Component2 {
r = 5;
size = 500;
points: Point[] = [];

constructor() {
var n = 10;
for (var i = 0; i < n; ++i) {
this.points.push({
x: Math.random() * this.size,
y: Math.random() * this.size
});
}
}

cx(point: Point) : number {
return point.x; // 何かすごい計算をする
}

cy(point: Point) : number {
return point.y; // 何かすごい計算をする
}
}

[attr.r]="r"という記述でcircle要素の属性をデータバインドしています。

Angular1のng-repeat="point in c2.points"*ngFor="#point of points"のようになっています。

また、@Componentの引数にchangeDetection: ChangeDetectionStrategy.OnPushを記述しています。

これはAngular2のChange Detectionの戦略をデフォルトから変更しています。

Change Detectionに関しては @laco0416 さんが既に詳しく書いてくれています。

http://qiita.com/laco0416/items/78edd53f5da8ead02e75

最後に、Appの実装です。

const appTemplate = `

<div>
<component1></component1>
<component2></component2>
</div>
`;

@Component({
selector: 'my-app',
template: appTemplate,
directives: [Component1, Component2, CORE_DIRECTIVES]
})
class App {
}

bootstrap(App);


Angular1とAngular2の違い

Angular1版とAngular2版の実装を少し比べてみましょう。

Angular2を意識してAngular1版を実装したこともありますが、基本的には同じような内容を実装できることがわかると思います。

Angular1ではComponentをdirectiveで実装していますが、directiveは汎用性が高い代わりにAPIが複雑と言われていました。

Angular2では@Componentを使うことでシンプルになっていると思います。

また、テンプレート構文でも属性データバインドのng-attr-r="{{c1.r}}[attr.r]="r"とシンプルになりました。

肝心のパフォーマンスに関してですが、Component2cxメソッドにconsole.logなどを挟んでみるとわかりますが、Angular1では、Component2とは独立しているComponent1messageが更新された場合でも毎回cxメソッドの呼び出しが入ります。

もしcxの中で重い処理をしている場合はそれが頻繁に呼び出されてパフォーマンスを悪化させてしまうことになります。

そのため、Angular1では重い処理をconstructorなどで事前に計算しておくといったコードが頻繁に現れていました。

今回はpointsComponent2の中で生成していますが、これを親コンポーネントから受け取る場合は、さらにその変更をScope.$watchで監視をするといった手間も必要になってきます。

この場合、コード量は増えるし、データの二重管理にもなるのであまりスマートではありませんでした。

Angular2でもデフォルトでは同じ動きをするようですが、Component2のDecoratorでchangeDetectionオプションを与えることでComponent2の状態に変化がない場合は要素の更新をスキップできます。

今回のrの変更など、Component2の状態に変化を加えた場合はしっかり反映が行われます。


おわりに

本稿では、Angular2を使ってとりあえずSVGを描いてみて、Angular1で不満だった点がどうなったか確認してみました。

現在公式チュートリアルが対応しているalpha.44では、SVGの構築がうまくいかなかったんですがalpha.45、46、49あたりでSVG対応が進み、現在のalpha.53では無事にSVGを描くことができました。

まだ細かい不具合はあるでしょうが、リリースに向けて順調に調整されていくでしょう。

また、性能という観点でも、データビジュアライゼーションでは、$watchの対象を減らすといったAngular1アプリでよくやるチューニングテクニックがあまり通用しないので、Change Detectionのような細かいチューニングの余地がAngular2で増えたことは非常にありがたいです。