Amazon DynamoDB のデータを API Gateway と Angular( D3.js ) でサーバーレス可視化する
データを溜め込んでいく理由はビジネス要件によって多々ありますが、要件のひとつに「データを可視化したい」というものがあると思います。今回は DynamoDB 上のデータを Angular と D3 を使ってサーバーレスで可視化するサンプルを作ってみます。
やること
- Angular + D3.js でデータ可視化の準備をする
- DynamoDB にデータを用意する
- API Gateway で Angular 向けに変換して DynamoDB のデータを返す
- 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に関するデータを、日別で見たいとしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 | 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 を使って棒グラフにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | 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 ) ; } ) ; } } |
実行すると以下のような結果が得られます。
さて、これを、DynamoDB のデータを使って表示できるよう、サーバーサイドを準備しましょう。
DynamoDB にデータを用意する
すでにデータが保存されている想定で、今回は手で追加してしまいます。 パーティションキーで candidateId
を指定し、同一候補者のデータを Query で取得できるようにしておきます。
API Gateway で Angular 向けに変換して DynamoDB のデータを返す
DynamoDB のAPIを叩いて得られる結果データは、そのままアプリケーションで利用するには若干パースの手間がかかるため、間に API Gateway を間に置くことにしました。
エンドポイントの作成
Angular から見て、
- https://xxxxx/candidates/{candidateId}/votes
- 例:https://apigateway.com/candidates/MI12341011/votes
このようなURLで 「特定の立候補者の投票ポイント一覧」 を得られるようにしましょう。{candidateId}
を指定することで DynamoDB 上のパーティションキーを指定できるようにします。リソースを下図のように作成してください。
リクエストの定義
リクエストの設定でやることは以下です。
- リクエストパスに含まれる
candidateId
を DynamoDB の Query 操作パラメータにする - DynamoDB への Query および そのリクエストボディを定義する
そして、「本文マッピングテンプレート」へ以下のように設定します。
1 2 3 4 5 6 7 | { "TableName" : "vote" , "KeyConditionExpression" : "candidateId = :a" , "ExpressionAttributeValues" :{ ":a" : { "S" : "$input.params('candidateId')" } } } |
レスポンスの定義
Query 操作の結果を、アプリケーション向けに少し加工します。こちらは、「統合レスポンス>マッピングテンプレート」で設定できます。
1 2 3 4 5 6 7 8 9 10 | #set($inputRoot = $input.path('$')) { "votes" : [ #foreach($elem in $inputRoot.Items) { "voteDate" : "$elem.voteDate.S" , "value" : "$elem.value.N" } #if($foreach.hasNext),#end #end ] } |
テストを実行して、以下のような結果が得られればOKです。
CORSの有効化
S3上のSPA(CoudFront経由で配信します)からのアクセスになりますので、CORSの設定が必要です。設定詳細については以下の記事を参考に設定してください。
API のデプロイ
ここまでできたら、API をデプロイします。実際にURLを叩いてみて、JSONが取得できるか試してみましょう。
Angular で ローカルデータを使う代わりにHTTPリクエストする
サーバーサイドの準備ができましたのでアプリケーション側に戻ります。現在、プログラム内にハードコードした配列を利用している状況ですので、Angular の Service 機能を使って HTTP 経由で先程のJSONを取得するよう修正します。
Service の作成と投票データ取得先の変更
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import { Injectable } from '@angular/core' ; import { Http } from '@angular/http' ; import { Vote } from './data' ; @Injectable ( ) export class VoteDataService { 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 を利用するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 | 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 のデータが取得できているようです。
ここで、DynamoDB にデータを追加してみます。
グラフを更新してみると、追加した分だけバーも増えていることがわかります。データの追加に対応することができました。
まとめ
DynamoDB と API Gateway を使って、DynamoDB のデータを取得するAPIを定義することができました。また、このレスポンス値を使って、S3に配備した AngularとD3.js製の SPA によって可視化することができました。
以前は、サーバーレスによるファイルアップロードシステムを作りました。今回はAWS上のデータを利用するパターンです。両者を組み合わせれば、データのアップロードから可視化までサーバーレスでできそうです。また実際に構築してみてまとめてみたいと思います。
ソースコード
バージョン情報
利用ツール・ライブラリ | バージョン |
---|---|
angular-core | 4.2.4 |
d3 | 4.11.0 |