Amazon DynamoDB のデータを API Gateway と Angular( D3.js ) でサーバーレス可視化する

2017.11.14

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

データを溜め込んでいく理由はビジネス要件によって多々ありますが、要件のひとつに「データを可視化したい」というものがあると思います。今回は DynamoDB 上のデータを Angular と D3 を使ってサーバーレスで可視化するサンプルを作ってみます。

visualize_app.png

やること

  1. Angular + D3.js でデータ可視化の準備をする
  2. DynamoDB にデータを用意する
  3. API Gateway で Angular 向けに変換して DynamoDB のデータを返す
  4. Angular で ローカルデータを使う代わりにHTTPリクエストする

Angular + D3.js でデータ可視化の準備をする

最初に、仮データを用意してローカルで可視化してしまいましょう。今回用意したデータはこちら。

候補者ID 投票受付日 投票ポイント
MI12341011 2017-11-10T12:00:12+09:00 345.11
MI12341011 2017-11-11T22:00:12+09:00 102.34
MI12341011 2017-11-12T09:12:45+09:00 344.11
SU40120055 2017-11-10T12:14:44+09:00 345.11

とある投票システムを仮想したデータです。特定の投票者IDに関するデータを、日別で見たいとしましょう。

data.ts

export interface Vote {
  voteDate: string;
  value: number;
}

export const Votes: Vote[] = [
  {voteDate: '2017-11-10T12:00:12+09:00', value: 345.11 },
  {voteDate: '2017-11-11T22:00:12+09:00', value: 102.34 },
  {voteDate: '2017-11-12T09:12:45+09:00', value: 345.11 },
  {voteDate: '2017-11-13T09:12:45+09:00', value: 312.12 },
  {voteDate: '2017-11-14T09:12:45+09:00', value: 267.34 }
];

これを D3.js を使って棒グラフにします。

vote-bar-chart.ts

import {Component, OnInit} from '@angular/core';

import * as d3 from 'd3-selection';
import * as d3Scale from 'd3-scale';
import * as d3Array from 'd3-array';
import * as d3Axis from 'd3-axis';
import * as d3TimeFormat from 'd3-time-format';
import {VoteDataService} from './vote-data.service';
import {Votes} from './data';

@Component({
  selector: 'app-vote-bar-chart',
  templateUrl: './vote-bar-chart.component.html',
  styleUrls: ['./vote-bar-chart.component.css']
})
export class VoteBarChartComponent implements OnInit {

  subtitle = 'Bar Chart';
  private margin = {top: 20, right: 20, bottom: 100, left: 50};
  private width: number;
  private height: number;
  private x: any;
  private y: any;
  private svg: any;
  private dateFormat = d3TimeFormat.timeFormat('%Y-%m-%d');

  constructor(private voteDataService: VoteDataService) {
    this.width = 900 - this.margin.left - this.margin.right;
    this.height = 500 - this.margin.top - this.margin.bottom;
  }

  ngOnInit() {
    this.initSvg();
    this.initAxis();
    this.drawAxis();
    this.drawBar();
  }

  private initSvg() {
    this.svg = d3.select('svg')
      .append('g')
      .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
  }

  /**
   * x軸: rangeRoundで対象領域を指定(利用可能領域いっぱい)、要素間の余白を0.35に指定
   * y軸: rangeで対象領域を指定
   * x軸ドメイン: データオブジェクトのvoteDateを使う
   * y軸ドメイン: データオブジェクトのvalueを使う。リニアで指定しているので最小値と最大値さえ渡せば良い。
   ****/
  private initAxis() {
    this.x = d3Scale.scaleBand().rangeRound([0, this.width]).padding(.35);
    this.y = d3Scale.scaleLinear().range([this.height, 0]);
    this.x.domain(this.Votes.map((d) => new Date(d.voteDate)));
    this.y.domain([0, d3Array.max(this.Votes, (d) => d.value)]);
  }

  private drawAxis() {
    /**
     * X軸描画。
     ****/
    this.svg.append('g')
      .attr('class', 'axis axis--x').attr('transform', 'translate(0,' + this.height + ')')
      .call(d3Axis.axisBottom(this.x).tickFormat(this.dateFormat))
      .selectAll('text')
      .style('text-anchor', 'end')
      .attr('dx', '-.8em').attr('dy', '-.55em').attr('transform', 'rotate(-90)');

    /**
     * Y軸描画。
     ****/
    this.svg.append('g')
      .attr('class', 'axis axis--y')
      .call(d3Axis.axisLeft(this.y));
  }

  /**
   * 棒グラフを描画。
   ****/
  private drawBar() {
    this.svg.selectAll('bar')
      .data(this.Votes)
      .enter().append('rect')
      .style('fill', 'DodgerBlue')
      .attr('class', 'bar')
      .attr('x', (d: any) => {
        return this.x(new Date(d.voteDate));
      })
      .attr('y', (d: any) => {
        return this.y(d.value);
      })
      .attr('width', this.x.bandwidth())
      .attr('height', (d: any) => {
        return this.height - this.y(d.value);
      });
  }
}

実行すると以下のような結果が得られます。

vote.png

さて、これを、DynamoDB のデータを使って表示できるよう、サーバーサイドを準備しましょう。

DynamoDB にデータを用意する

すでにデータが保存されている想定で、今回は手で追加してしまいます。 パーティションキーで candidateId を指定し、同一候補者のデータを Query で取得できるようにしておきます。

dynamodb.png

API Gateway で Angular 向けに変換して DynamoDB のデータを返す

DynamoDB のAPIを叩いて得られる結果データは、そのままアプリケーションで利用するには若干パースの手間がかかるため、間に API Gateway を間に置くことにしました。

エンドポイントの作成

Angular から見て、

  • https://xxxxx/candidates/{candidateId}/votes
  • 例:https://apigateway.com/candidates/MI12341011/votes

このようなURLで 「特定の立候補者の投票ポイント一覧」 を得られるようにしましょう。{candidateId} を指定することで DynamoDB 上のパーティションキーを指定できるようにします。リソースを下図のように作成してください。

apigateway_resources.png

リクエストの定義

リクエストの設定でやることは以下です。

  • リクエストパスに含まれる candidateId を DynamoDB の Query 操作パラメータにする
  • DynamoDB への Query および そのリクエストボディを定義する

apigateway_req_def.png

そして、「本文マッピングテンプレート」へ以下のように設定します。

application/json

{
    "TableName": "vote",
    "KeyConditionExpression": "candidateId = :a",
    "ExpressionAttributeValues":{
       ":a": { "S" : "$input.params('candidateId')"}
    }
}

レスポンスの定義

Query 操作の結果を、アプリケーション向けに少し加工します。こちらは、「統合レスポンス>マッピングテンプレート」で設定できます。

application/json

#set($inputRoot = $input.path('$'))
{
    "votes": [
        #foreach($elem in $inputRoot.Items) {
            "voteDate": "$elem.voteDate.S",
            "value": "$elem.value.N"
        }#if($foreach.hasNext),#end
	#end
    ]
}

テストを実行して、以下のような結果が得られればOKです。

apigateway_req_def.png

CORSの有効化

S3上のSPA(CoudFront経由で配信します)からのアクセスになりますので、CORSの設定が必要です。設定詳細については以下の記事を参考に設定してください。

API のデプロイ

ここまでできたら、API をデプロイします。実際にURLを叩いてみて、JSONが取得できるか試してみましょう。

Angular で ローカルデータを使う代わりにHTTPリクエストする

サーバーサイドの準備ができましたのでアプリケーション側に戻ります。現在、プログラム内にハードコードした配列を利用している状況ですので、Angular の Service 機能を使って HTTP 経由で先程のJSONを取得するよう修正します。

Service の作成と投票データ取得先の変更

vote-data.service.ts

import {Injectable} from '@angular/core';
import {Http} from '@angular/http';
import {Vote} from './data';

@Injectable()
export class VoteDataService {

  private voteBase = 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1/candidates';
  private headers = new Headers({'Content-Type': 'application/json'});

  constructor(private http: Http) {
  }

  getVotes(candidateId: string): Promise<Vote[]> {
    return this.http.get(this.voteUrl(candidateId), this.headers)
      .toPromise()
      .then(response => response.json().votes as Vote[])
      .catch(this.handleError);
  }

  private voteUrl(candidateId: string): string {
    return `${this.voteBase}/${candidateId}/votes`;
  }

  private handleError(error: any): Promise<any> {
    console.error('An error occurred', error);
    return Promise.reject(error.message || error);
  }
}

Component 側は、Service 経由で取得した Vote を利用するようにします。

vote-bar-chart.component.ts

ngOnInit() {
  this.initData().then(() => {
    this.initSvg();
    this.initAxis();
    this.drawAxis();
    this.drawBar();
  });
}

private initData(): Promise<Vote[]> {
  return this.voteDataService.getVotes('MI12341011').then(votes => this.Votes = votes);
}

SPA のデプロイ

作成した Angular アプリケーションは、S3 にアップロードし、CloudFront を介してホスティングします。具体的な手順については以下を参考ください。

SPA を使ってみる

CloudFront で配信したアプリケーションにアクセスしてみます。裏側で API Gateway へアクセスし、DynamoDB のデータが取得できているようです。

vote-chart-cloudfront.png

ここで、DynamoDB にデータを追加してみます。

vote-add-dynamodb.png

グラフを更新してみると、追加した分だけバーも増えていることがわかります。データの追加に対応することができました。

bar-chat-after.png

まとめ

DynamoDB と API Gateway を使って、DynamoDB のデータを取得するAPIを定義することができました。また、このレスポンス値を使って、S3に配備した AngularとD3.js製の SPA によって可視化することができました。

以前は、サーバーレスによるファイルアップロードシステムを作りました。今回はAWS上のデータを利用するパターンです。両者を組み合わせれば、データのアップロードから可視化までサーバーレスでできそうです。また実際に構築してみてまとめてみたいと思います。

ソースコード

バージョン情報

利用ツール・ライブラリ バージョン
angular-core 4.2.4
d3 4.11.0

参考