【Angular】お天気アプリを開発してみた ‐ 開発備忘録 #2

こんにちは、Smart Designの伊藤です。
前回の記事（[開発備忘録 #1](https://qiita.com/SmartDesign/items/c5ee6245794fdc7456d6)）では、Angularを使ってお天気アプリを開発するための事前準備を紹介しました。
今回はその続編として、
- **取得した天気データとそのグラフの表示（Chart.js）**

を中心に紹介していきます。


# 5.グラフで天気を可視化
天気予報といえば、数値だけでなく「視覚的なグラフ表示」があると分かりやすくなりますよね。このアプリでも、Chart.js を用いて、天気データ（気温）を折れ線グラフで可視化する機能を実装しました。
ここでは、
　・**どのような手順で天気情報を取得しグラフ表示しているか**

　・**使用しているコンポーネントとその役割**

　・**各コンポーネントで実行している処理**

について解説します。

実際の画面
![スクリーンショット 2025-05-27 130532.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3991405/87094668-e149-4746-b887-5d2a073b82a1.png)

-------------------------------------------------------------------------------------

### 5-1.表示までの流れ
1.`weather.component`がフォームから都市名を受け取る

2.`weather.service.ts`で OpenWeatherMap API にリクエストを送り、天気データを取得

3.取得データを整形し、テンプレートで表示

4.同時に`weather-chart.component`に気温の時系列データを渡し、グラフ表示を行う

-------------------------------------------------------------------------------------

### 5-2. コンポーネント
`weather.component`（親）
<table>
  <thead>
    <tr>
      <th>ファイル</th> <th>説明</th>
    </tr>
  </thead>
  <tr>
    <td> weather.component.ts </td>
    <td>ユーザーの入力（都市名）を受け取り、APIリクエストを実行。結果データをテンプレートと子コンポーネントへ渡す。</td>
  </tr>
  <tr>
    <td> weather.component.html </td>
    <td>検索フォームと天気データ表示、<app-weather-chart> を配置し、チャートデータを渡す。</td>
  </tr>
</table>

`weather-chart.component`（子）
<table>
  <thead>
    <tr>
      <th>ファイル</th> <th>説明</th>
    </tr>
  </thead>
  <tr>
    <td> weather-chart.component.ts </td>
    <td> @ Input() で受け取った気温データを元に Chart.js グラフを描画。※ここで表記されているInput()は、リンクを消すためにわざとスペースを入れています</td>
  </tr>
  <tr>
    <td> weather-chart.component.html </td>
    <td>Chart.js コンポーネントをテンプレート内に定義。</td>
  </tr>
</table>

`weather.service.ts`（サービス）
<table>
  <thead>
    <tr>
      <th>ファイル</th> <th>説明</th>
    </tr>
  </thead>
  <tr>
    <td> weather.service.ts </td>
    <td>OpenWeatherMap API など外部APIとの通信を担う。HttpClient を用いて天気データを取得し、Observable で返す。</td>
  </tr>
</table>

-------------------------------------------------------------------------------------

### 5-3.各コンポーネントの処理の流れ
### `weather.component.ts`
```diff_typescript:weather.component.ts
export class WeatherComponent {
  weatherData: any;
  chartData: number[] = [];
  chartLabels: string[] = [];

  constructor(private weatherService: WeatherService) {}

  search(city: string) {
    this.weatherService.getWeather(city).subscribe(data => {
      this.weatherData = data;

      // グラフ用のデータ抽出（例: 3時間ごとの気温）
      this.chartLabels = data.list.map((item: any) =>
        new Date(item.dt_txt).getHours() + '時'
      );
      this.chartData = data.list.map((item: any) =>
        Math.round(item.main.temp)
      );
    });
  }
}
```
### `WeatherComponent` で行っている処理
このコンポーネントは、天気検索とグラフ表示の中核的な役割を担っています。主な処理の流れは以下の通りです：

**1. ユーザーが都市名を入力して検索を実行**
`search(city: string)` メソッドが呼び出され、入力された都市名を引数に `WeatherService` を通じて API リクエストを送信。

**2. 取得した天気データを受け取り保持**
`this.weatherData` に API から返された天気予報（5日分/3時間刻みのデータ）をそのまま保持。

**3. グラフ描画用のデータを抽出・整形**
`chartLabels`: 各予報の時刻を「○時」の形式に変換し、横軸のラベルとして使う。
`chartData`: 各予報時点の気温を取り出して、縦軸のデータセットとして使用。

これらはそのまま子コンポーネントの weather-chart に渡され、グラフ表示に利用されます。

### `weather.component.html`
```diff_html:html
<form (ngSubmit)="search(cityInput.value)">
  <input #cityInput type="text" placeholder="都市名を入力" />
  <button type="submit">検索</button>
</form>

<!-- 天気の概要表示 -->
<div *ngIf="weatherData">
  <h2>{{ weatherData.city.name }} の天気</h2>
  <p>現在の気温: {{ weatherData.list[0].main.temp }}℃</p>

  <!-- グラフ表示コンポーネント -->
  <app-weather-chart
    [labels]="chartLabels"
    [data]="chartData">
  </app-weather-chart>
</div>
```
### `weather.component.html` の構成と処理の流れ

**1. 天気検索フォーム**
```diff_html:html
<input type="text" [(ngModel)]="city" placeholder="都市名を入力" />
<button (click)="search(city)">検索</button>
```
- **双方向バインディング**（[(`ngModel`)]） により、入力欄と `city` 変数が常に同期
- 検索ボタン押下で `search()` メソッドを呼び出し、APIリクエストを実行

-------------------------------------------------------------------------------------

**2. 天気情報の表示（検索結果）**
```diff_html:html
<div *ngIf="weatherData">
  <h2>{{ weatherData.city.name }} の天気</h2>
  <ul>
    <li *ngFor="let item of weatherData.list.slice(0, 5)">
      {{ item.dt_txt }} - {{ item.weather[0].description }} - {{ item.main.temp }}°C
    </li>
  </ul>
</div>
```
- `*ngIf` で天気データがある場合のみ結果を表示
- `*ngFor` で最初の数件（例：5件）の予報を簡易表示
- **日時・天気概要・気温** をシンプルに出力

-------------------------------------------------------------------------------------

**3. グラフコンポーネントの表示**
```diff_html:html
<app-weather-chart
  [labels]="chartLabels"
  [data]="chartData">
</app-weather-chart>
```
- 子コンポーネント `WeatherChartComponent` をテンプレート内に配置
- `@Input()` で渡された `labels`（時刻ラベル）と `data`（気温データ）を使ってグラフを描画

-------------------------------------------------------------------------------------

### `weather-chart.component.ts`
```diff_typescript:weather-chart.component.ts
import { Component, Input, OnChanges } from '@angular/core';

@Component({
  selector: 'app-weather-chart',
  templateUrl: './weather-chart.component.html',
  styleUrls: ['./weather-chart.component.css']
})
export class WeatherChartComponent implements OnChanges {
  @Input() data: number[] = [];
  @Input() labels: string[] = [];

  public chart: any;

  ngOnChanges() {
    this.renderChart();
  }

  renderChart() {
    this.chart = {
      type: 'line',
      data: {
        labels: this.labels,
        datasets: [{
          label: '気温 (℃)',
          data: this.data,
          borderColor: 'rgba(75, 192, 192, 1)',
          fill: false,
        }]
      },
      options: {
        responsive: true,
        plugins: {
          legend: { display: true },
        },
        scales: {
          y: {
            beginAtZero: false
          }
        }
      }
    };
  }
}
```
### `weather-chart.component.ts` の処理
このコンポーネントは、**親コンポーネントから渡された天気データをグラフで可視化する専用パーツ**です。表示ロジックに特化し、グラフ描画を担当します。

**主な役割と処理**
```diff_typescript:weather-chart.component.ts
@Input() labels: string[] = [];
@Input() data: number[] = [];
```
- 親コンポーネント（`WeatherComponent`）から、**グラフのラベル（時刻）とデータ（気温）を受け取る**
- `@Input()` デコレーターで双方向バインディングなしに受け取る「受信専用プロパティ」を定義

------------------------------------

**グラフライブラリへのデータ提供**
```diff_typescript:weather-chart.component.ts
chartOptions = {
  responsive: true,
  plugins: { legend: { display: false } }
};

chartData = [{ data: [], label: '気温' }];
chartLabels: string[] = [];
```
- `ngOnChanges()` などで `@Input()` で受け取ったデータを、グラフ用の構造に整形
- 使用しているグラフライブラリ（Chart.js、ng2-charts）に合わせた構造へ変換して渡す

-------------------------------------

**`weather-chart.component.html`**
```diff_html:weather-chart.component.html
<canvas baseChart
  [data]="chart.data"
  [type]="chart.type"
  [options]="chart.options">
</canvas>
```
このテンプレートは、**グラフライブラリ（Chart.js + ng2-charts）を使って気温の変化を可視化するビュー**です。描画部分は非常にシンプルながら、動的なデータバインディングに対応しています。

**処理のポイント**
- `baseChart` ディレクティブは、**ng2-charts ライブラリ**のコンポーネント機能
- `[data]`：グラフに表示する系列データ（例：気温）をバインド
- `[type]`：グラフの種類（例：'line', 'bar' など）を指定
- `[options]`：凡例や軸の表示設定などの各種カスタマイズオプション

**データの流れ**
1. `weather.component.ts` で都市の天気データを取得
2. `weather-chart.component.ts` で `@Input()` を通じて受け取ったデータを `chart.data` に変換
3. テンプレートの `<canvas baseChart>` に反映され、**気温のグラフが自動描画**される

-----------------------------------------------

### `weather.service.ts`
```diff_typescript:weather.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class WeatherService {
  private apiUrl = 'https://api.openweathermap.org/data/2.5/forecast';

  constructor(private http: HttpClient) {}

  getWeather(city: string): Observable<any> {
    return this.http.get(this.apiUrl, {
      params: {
        q: city,
        units: 'metric',
        appid: environment.weatherApiKey
      }
    });
  }
}
```
### 天気データを取得するサービス：`WeatherService`
`WeatherService` は、アプリケーション内の他のコンポーネントから呼び出され、**OpenWeatherMap の天気予報 API** にアクセスして天気情報を取得するサービスです。HTTP通信の実装を専用のクラスに分離することで、保守性とテスト性が向上します。

**主な役割と処理**
1.サービスとしてアプリに登録
```diff_typescript:weather.service.ts
@Injectable({ providedIn: 'root' })
```
- この1行で、アプリ全体でこのサービスを**シングルトンとして利用可能**にした
- 各コンポーネントで DI（依存性注入）を通じて呼び出すだけで使える

2.HTTP通信の実装
```diff_typescript:weather.service.ts
constructor(private http: HttpClient) {}
```
- Angular の `HttpClient` を使って外部 API にアクセス
- 必要なモジュールは `HttpClientModule` を `AppModule` にインポートして使用

3.天気APIの呼び出し
```diff_typescript:weather.service.ts
getWeather(city: string): Observable<any> {
  return this.http.get(this.apiUrl, {
    params: {
      q: city,
      units: 'metric',
      appid: environment.weatherApiKey
    }
  });
}
```
- `city` を引数に取り、OpenWeatherMap API に GET リクエストを送信
- **メトリック単位（摂氏）で取得**し、APIキーは環境変数 `environment.weatherApiKey` から安全に参照
- 戻り値は `Observable<any`> 型で返却され、呼び出し元（コンポーネント）で `.subscribe()` によって非同期的に取得・処理



------------------------------------------------

### 5-4.まとめ
このように、**親コンポーネントでAPIから取得した天気データを加工し、子コンポーネントに渡す**ことで、Angularらしい責務分離と再利用性のある設計が実現できました。

- データ取得はサービス層に集約（`weather.service.ts`）

- 表示とグラフ描画はコンポーネントごとに責任分担

- `@Input()` によるデータの受け渡しで柔軟な連携

次回は、検索履歴の保存や、位置情報との連携について掘り下げていきます。