RESAS-APIから人口ピラミッドデータを取得してHighchartsでグラフ表示する。

概要

グラフ表示部分については、再利用を考慮して別コンポーネントを作成。

1
2
3
4
5
6
component/
 ├ highcharts/
 │ └ PyramidChart.vue
 ├ populationPyramid/
   └ PopulationPyamidPref.vue

都道府県の人口構成データの取得

パラメータの設定

API概要のとおりURLとパラメータを設定する。

都道府県については、親コンポーネントからpropsで受け取る。型は{prefCode:number, prefName:string}

PopulationpyramidPref.vue#L44-L49link
44
45
46
47
48
49
props: {
selectedPref: {
type: Object,
required: true,
},
},

オブジェクトからprefCodeを取得し、resasParamsにセットする。
都道府県全体の人口を利用したいので、cityCodeは-
年次についてはyearLeftyearRightの2種類指定できる。

PopulationpyramidPref.vue#L67-L73link
67
68
69
70
71
72
73
resasParams() {
return {
prefCode: String(this.prefCode),
cityCode: '-',
yearLeft: String(this.year),
yearRight: '2040',
}

年次の設定

yearRightは2040で固定して、yearLeftをスライダーで選択する。

PopulationpyramidPref.vue#L13-L22link
13
14
15
16
17
18
19
20
21
22
<v-slider
v-model="year"
thumb-label="always"
class="Slider"
:max="2045"
:min="1980"
step="5"
hide-details
@change="$emit('input', $event)"
/>

スライダーの値に応じてthis.yearLeftの値が変わる。watchで検出して再度fetchすることを忘れないように。

PopulationpyramidPref.vue#L94-L98link
94
95
96
97
98
watch: {
year() {
this.$fetch()
},
},

axiosでデータ取得

Nuxtのfetchメソッドを利用してRESAS-APIの値を取得する。

axiosの共通設定はプラグイン化しているので、this.$resas.getでデータを取得する。

プラグインについては下記の記事を参照

PopulationPref.vue#L32-L37link
32
33
34
35
36
37
async fetch() {
const url = this.resasUrl
const params = this.resasParams
const { data } = await this.$resas.get(url, { params })
this.resasResponse = data
},

これでresasResponseに結果が格納される。

1
2
3
4
5
data() {
return {
resasResponse: null,
}
},

なお、APIを取得した後にページを表示させる必要があるので、$fetchState.pendingを忘れてはいけない。

PopulationpyramidPref.vue#L3-L5link
3
4
5
<v-card :loading="$fetchState.pending">
<p v-if="$fetchState.pending" />
<div v-else>

データの整形

レスポンスのデータはAPI概要のとおり。

今回はHighcharts.jsのBar with negative stackを利用したいので、取得したデータをHighhartsで利用できる形に整える。

PopulationpyramidPref.vue#L78-L88link
78
79
80
81
82
83
84
85
86
87
88
chartData() {
const data = this.resasResponse.result.yearLeft.data
return data.map((d) => {
return {
category: d.class,
man: d.man,
woman: d.woman,
unit: this.unit,
}
})
},

グラフ表示

最後に、this.displayDataの値をColumnChartコンポーネントに渡してグラフを表示させる。

※chartDataをそのまま渡してもよいが、ColumnChartを利用する場合等に合わせて、グラフ表示するデータはdisplayDataに統一する。

まとめ

これまでをまとめると、RESAS-APIからデータを取得して加工するまでのコンポーネントは次のとおりとなる。

PopulationpyramidPref.vuelink
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
98
99
100
101
<template>
<v-col cols="4" sm="8" md="6">
<v-card :loading="$fetchState.pending">
<p v-if="$fetchState.pending" />
<div v-else>
<!-- タイトル -->
<v-card-title class="headline">
{{ title }}
</v-card-title>

<v-card-text>
<!-- スライダー -->
<v-slider
v-model="year"
thumb-label="always"
class="Slider"
:max="2045"
:min="1980"
step="5"
hide-details
@change="$emit('input', $event)"
/>

<!-- グラフ -->
<HighchartsPyramidChart :display-data="displayData" />

<!-- 注釈 -->
<hr class="my-3" />
<p>地域経済分析システムRESASのAPIを利用して作成</p>
</v-card-text>
</div>
</v-card>
</v-col>
</template>

<script>
export default {
async fetch() {
const url = this.resasUrl
const params = this.resasParams
const { data } = await this.$resas.get(url, { params })
this.resasResponse = data
},
props: {
selectedPref: {
type: Object,
required: true,
},
},
data() {
return {
resasResponse: null,
year: 2015,
unit: '人',
}
},
computed: {
prefCode() {
return this.selectedPref.prefCode
},
prefName() {
return this.selectedPref.prefName
},
resasUrl() {
return `api/v1/population/composition/pyramid`
},
resasParams() {
return {
prefCode: String(this.prefCode),
cityCode: '-',
yearLeft: String(this.year),
yearRight: '2040',
}
},
title() {
return `${this.prefName}の人口ピラミッド`
},
chartData() {
const data = this.resasResponse.result.yearLeft.data
return data.map((d) => {
return {
category: d.class,
man: d.man,
woman: d.woman,
unit: this.unit,
}
})
},
displayData() {
return this.chartData
},
},
created() {},
watch: {
year() {
this.$fetch()
},
},
}
</script>

PyramidChartコンポーネントの作成

受け取ったdisplayDataを元に、Highchart.jsでグラフ描画する。

コード全文

PyramidChart.vuelink
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
<template>
<div class="graph">
<highcharts :options="chartOptions" />
</div>
</template>

<script>
import { cloneDeep } from 'lodash'

export default {
props: {
displayData: {
type: Array,
required: true,
},
},
data() {
return {
// charWidth: 300,
// windowWidth: 0,
}
},
computed: {
categories() {
return this.displayData.map((d) => d.category)
},
unit() {
return this.displayData[0].unit
},
series() {
return [
{
name: '男性',
data: this.displayData.map((d) => -1 * d.man),
color: '#4169e1',
},
{
name: '女性',
data: this.displayData.map((d) => d.woman),
color: '#ff69b4',
},
]
},
chartOptions() {
return {
chart: {
height: 350,
type: 'bar',
backgroundColor: 'transparent',
},
title: {
text: null,
},
accessibility: {
point: {
valueDescriptionFormat: '{index}. {xDescription}, {value}.',
},
},
xAxis: [
{
// 左軸
categories: this.categories,
reversed: false,
labels: {
step: 1,
},
accessibility: {
description: '年齢(男性)',
},
},
{
// 右軸
opposite: true,
reversed: false,
categories: this.categories,
linkedTo: 0,
labels: {
step: 1,
},
accessibility: {
description: '年齢(女性)',
},
},
],
yAxis: {
title: {
text: null,
},
labels: {
formatter() {
return this.value.toLocaleString()
},
},
},
plotOptions: {
series: {
animation: false,
stacking: 'normal',
},
},
responsive: {
rules: [
{
condition: {
maxheight: 350,
},
chartOptions: {
legend: {
layout: 'horizontal',
align: 'right',
verticalAlign: 'top',
},
},
},
],
},
tooltip: {
crosshairs: true,
shared: true,
useHTML: true,
formatter() {
return this.points.map((point) => {
return `
<i style="
background-color:${point.color};
border-radius:50%;
display: inline-block;
height:6px;
margin-right:4px;
width:6px;"
></i>${point.series.name}: <b>${Math.abs(
point.y
).toLocaleString()}人</b><br>`
})
},
},
credits: {
enabled: false,
},
series: this.series,
}
},
},
}
</script>

<style lang="sass" scoped>
.graph
margin-top: 10px
height: 100%
</style>