Apple WatchのデータをブログのグラフにするまでのTech Stack


前の記事で掲載したVO2Maxと安静時心拍のグラフ、どうやって作ったか聞かれたのでまとめておく。

全体の流れ

Apple Watch
    ↓ 自動記録
Apple Health アプリ
    ↓ XMLエクスポート → パース
DuckDB(ローカルDB)
    ↓ SQLクエリ(MCP経由)
Claude(AI)が数値を取得・分析
    ↓ MDXに埋め込み
Chart.js + Astro でグラフ表示

1. Apple Health → DuckDB

Apple WatchはVO2Max・安静時心拍・HRVなどを自動で計測し、iPhoneのHealthアプリに蓄積している。これをエクスポートしてローカルのDuckDBに取り込んでいる。

DuckDBはSQLが使える軽量な組み込みDBで、Pythonやコマンドラインから手軽に使える。Health データは hkquantitytypeidentifiervo2max_2026 のような名前のテーブルに格納されている。

2. MCP(Model Context Protocol)でSQLクエリ

MCP(Model Context Protocol)はClaudeなどのAIがローカルのツールやDBと連携するための仕組み。apple-health MCPサーバーを使うことで、ClaudeがDuckDBに直接SQLクエリを投げられる。

月別平均を出すのに使ったクエリはこんな感じ。

SELECT
  strftime('%Y-%m', startDate) as month,
  ROUND(AVG(value), 1) as avg_vo2max
FROM hkquantitytypeidentifiervo2max_2026
WHERE startDate >= '2025-01-01'
GROUP BY month
ORDER BY month ASC

このクエリをAIが実行→数値を取得→記事の原稿に埋め込む、という流れをほぼ自動でやってくれた。

3. Chart.js + Astro でグラフ表示

グラフの描画にはChart.jsを使っている。AstroのMDX記事の中でコンポーネントとして呼び出せるよう、LineChart.astro というコンポーネントを作った。

---
// src/components/LineChart.astro
interface Props {
  id: string;
  labels: string[];
  datasets: { label: string; data: number[]; color: string; }[];
  yLabel?: string;
  yMin?: number;
  yMax?: number;
}
---
<canvas id={id} data-labels={JSON.stringify(labels)} ... />
<script>
  import { Chart, ... } from 'chart.js';
  // canvasのdata属性からデータを読み取ってグラフ描画
</script>

MDX記事側はこれだけで呼び出せる。

import LineChart from '../../components/LineChart.astro';

<LineChart
  id="vo2max-chart"
  labels={["2025-01", "2025-02", ...]}
  datasets={[{
    label: "VO2Max(月平均)",
    data: [34.4, 33.2, ...],
    color: "#e76f51"
  }]}
  yMin={28}
  yMax={38}
/>

データをコンポーネントのpropsとして直接渡せるのでシンプル。グラフを増やしたいときは id を変えて追加するだけ。

まとめ

レイヤー使ったもの
データ収集Apple Watch + Health アプリ
データ保存DuckDB
データ取得Apple Health MCP + SQL
グラフ描画Chart.js
ブログAstro + MDX
ホスティングCloudflare Pages

「Apple Watchのデータをブログに載せる」というだけなら以前は相当な手間がかかったと思う。MCPでAIがDBに直接クエリできる環境があると、データ取得から記事化まで会話の流れでできてしまう。

このブログ自体もAstro + Cloudflare Pagesで作ったので、次は構築の話も書くかもしれない。