オープンデータを使って「課題を解決する Web アプリ」を作れるようになる!
「GW に恐竜博物館に行きたいけど、いつが空いてる?」 → データで調べて、アプリで見える化する
Visual Studio Code は、 HTML・CSS・JavaScript などのコードを書くためのエディタ
今日の実習では主にこれを使う:
左側の四角いアイコンから拡張機能パネルを開く
左側のサイドバーにある 拡張機能アイコン をクリックする
検索窓に拡張機能名を入力して、 表示された候補の インストール を押す
コマンドパレット Ctrl(cmd) + Shift + P から 基本設定: ワークスペース設定を開く (JSON) を選ぶ
Ctrl(cmd) + Shift + P
基本設定: ワークスペース設定を開く (JSON)
{ "editor.wordWrap": "on", "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.renderWhitespace": "all", "editor.tabSize": 2, "editor.minimap.enabled": false }
次のような効果があります。
index.html
styles.css
app.js
Ctrl(cmd) + S
「編集する場所」と「実行結果を見る場所」を行き来する のが基本
たとえると、HTML は部屋に置く家具や配置、CSS は壁紙やインテリア、 JavaScript はスイッチを押したら電気がつくような「動くしくみ」
index.html ← 骨格(グラフ用 canvas、スポット一覧の枠など) styles.css ← 見た目(2カラムレイアウト、カードのスタイル) app.js ← 動き(データ取得・グラフ描画・スポット絞り込み)
開発者ツール(F12)で確認しよう
<canvas id="reservation-chart">
オープンデータ = 誰でも自由に使えるデータ
「データがあるからアプリが作れる」という発想の転換が大切
ポイント:
「どんなデータがあるか」より先に、 何を判断したいか を考える
たとえば今日なら:
問いが決まったら、次は見える化する
template/
教材リポジトリの template/ から、次の 3 ファイルを自分の作業フォルダへコピーして始めます(フォルダごとコピーでもよい)。
VSCode でフォルダを開き、Live Server で index.html を表示できる状態にします。
Web で広く使われるグラフライブラリのひとつ
インターネット上の 1 ファイルを、index.html から <script src="..."> で読み込むだけで使い始められます。 Chart.js を先に読み込んでから app.js を読み込みます(順番が逆だと Chart が未定義になります)。
<script src="...">
Chart
data
{ labels: ["5/1", "5/2", "5/3"], // 横軸ラベル(文字列の配列) datasets: [{ label: "予約人数", // 凡例に表示される名前 data: [120, 300, 2502], // 棒の高さ(数値の配列) backgroundColor: "green", // 棒の色 }], }
labels と data の長さは必ず一致させる
labels
// app.js の buildOrUpdateChart() より const labels = rows.map((r) => formatDateLabel(r.date_visit)); // 日付 → "M/D" const data = rows.map((r) => r.n_people); // 予約人数 const backgroundColor = rows.map((r) => barColor(r.n_people)); // 色分け
map で配列の全要素に変換処理を適用している
map
→ CSV から読んだ全日分が自動でグラフに入る
ここで触っているのは、予約人数グラフを作るための処理です。 「横軸の文字」と「実際に描く処理」を順番に有効にします。 色分けはまだ入れず、パート 7 で追加します。
main()
console.log(...)
log...Try()
[4-1] formatDateLabel
2026-05-03
5/3
logFormatDateLabelTry()
isoDate
"2026-05-03"
条件
"-"
m
d
"05"
5
"M/D"
[4-2] buildOrUpdateChart
rows
{ date_visit, n_people, amount_fee }
app.js の /* と */ の行を削除して有効にする
/*
*/
関数によって手順が違います。
// STUDENT:
// 外す前 /* function buildOrUpdateChart(...) { ... } */ // 外した後(/* と */ の行だけ削除) function buildOrUpdateChart(...) { ... }
複数をまとめて外さず、スライドに出てきた順で 1 つずつ進める
fetch
await
console
split
trim
filter
str.trim()
" hi \n".trim()
"hi"
str.split(sep)
"a,b,c".split(",")
["a","b","c"]
arr.map(fn)
[1,2,3].map((n) => n * 2)
[2,4,6]
arr.filter(fn)
[1,2,3].filter((n) => n > 1)
[2,3]
Step 3 の「抽出」では、この 4 つをつなげて CSV 文字列 → 行 → オブジェクト にしていきます。
今まで:グラフのデータを ハードコード(コードに直書き)していた
data: [120, 300, 2502, 1800, 400], // 手で書いたデータ
問題:
→ URL からデータを自動で取ってきたい!
// URL を渡すとデータが返ってくる const response = await fetch("https://example.com/data.csv"); const text = await response.text(); console.log(text); // CSV のテキスト
fetch はネットワーク越しにデータを取得する組み込み関数 response.text() でテキストとして受け取る
response.text()
async
イメージ: フードコートで料理を待つのに近い
fetch(...)
res.text()
res.json()
「待つ必要がある処理」を、上から順に読みやすく書けるのが async / await
async function loadData() { // async をつけると await が使える const res = await fetch(URL); // ← ここで待つ const text = await res.text(); // ← テキストが届いたら進む console.log(text); } loadData(); // 呼び出し
await は async 関数の中でしか使えない
DINO_CSV_URL のような 公開 URL に fetch → await で待ち、await res.text() で CSV 全体を 1 本の文字列として受け取ります(まだ「表」ではない段階)。
DINO_CSV_URL
await res.text()
例(latest_dino_sum.csv の先頭付近・実データは更新されます):
date_visit,n_people,amount_fee 2026-04-16,0,0 2026-04-17,763,589850 2026-04-18,1724,1402450
…(以下、同形式の行が続き、改行で 1 本の文字列)
いきなりパースせず、生の文字列を一度見ます。
console.log(text.slice(0, 200));
\n
\r\n
「どんな形のテキストが届いたか」がわかると、あとの抽出が迷わなくなる
文字列のままではグラフに渡せないので、行の配列 → 1 行ずつオブジェクト に変換します。
splitLines
parseDinoCsv
fetch → await res.text() で届くのは CSV 全体を 1 本にした文字列(まだ配列でもオブジェクトでもない)。このあと 行ごとにオブジェクトへ変換する関数を書く。例(latest_dino_sum.csv 先頭付近・更新あり)。
…(以下、同形式の行が続く)
parseDinoCsv(text) を定義します。前スライドのような CSV 文字列を仮引数 text に渡すと、オブジェクトの配列が返るようにします。
parseDinoCsv(text)
text
function parseDinoCsv(text) { const normalized = text.trim().replaceAll("\r", ""); const lines = normalized.split("\n"); const rows = lines .slice(1) .filter((line) => line !== "") .map((line) => { const [date_visit, n_people] = line.split(","); return { date_visit, n_people: Number(n_people) || 0, }; }); return rows; }
replaceAll("\r", "")
\r
split("\n")
slice(1)
split(",")
\r の例: 改行が \r\n のままだと、\n だけで行に分けても行末に \r が残る。line.split(",") の列が "3" ではなく "3\r" になり、Number("3\r") は NaN になる。先に \r を消しておくと防げる。
line.split(",")
"3"
"3\r"
Number("3\r")
NaN
loadDinoData()
async function loadDinoData() { const res = await fetch(DINO_CSV_URL); // 1. CSV を取得 const text = await res.text(); // 2. テキストとして受け取る const rows = parseDinoCsv(text); // 3. 配列に変換 buildOrUpdateChart(rows); // 4. グラフに渡す renderTopThree(rows); // 5. TOP 3 を表示 }
データが流れる順番をトレースしてみよう
CSV テキスト → parseDinoCsv → オブジェクト配列 → buildOrUpdateChart → グラフ
buildOrUpdateChart
コードは splitLines → parseDinoCsv → loadDinoData の順で進めます(後ろの関数が前の関数を使うため)。
loadDinoData
3 ステップとの対応
[5-3] loadDinoData
console.log
[5-3]
[5-1]
[5-2]
ここで触っているのは、公開 CSV を読んで予約人数グラフに流し込むための処理です。
[5-1] splitLines
logSplitLinesTry()
"\r"
"\n"
""
[5-2] parseDinoCsv
date_visit
n_people
logParseDinoCsvTry()
splitLines(text)
app.js の // STUDENT: の指示を読み、次の条件を満たすコードを書いてください。
","
amount_fee
Number()
0
Promise<void>
dinoRows
res.ok
false
throw new Error(...)
Day 2 では、スポット一覧や色分け、公開 API も加えて、 「行く日を決めやすいアプリ」に近づける
「博物館の混雑はわかった。せっかく行くなら近くのスポットも知りたい」
→ fukui-spot データ を使ってスポット一覧を追加する
スポットデータは列数が多いため、ライブラリを使う
Papa.parse(SPOTS_CSV_URL, { download: true, header: true, // 先頭行をキーとして使う → オブジェクト配列に skipEmptyLines: true, complete: (results) => { console.log(results.data); // [{ name: "...", category: "...", ... }, ...] }, });
列数が少なく固定(恐竜 CSV)→ split で手書きパース 列数が多い・増える可能性あり(スポット CSV)→ Papa Parse
問題に合った道具を選ぶのが大切
function spotCardFromRow(row) { const card = document.createElement("article"); // 要素作成 card.className = "spot-card"; const h3 = document.createElement("h3"); h3.textContent = row.name; card.appendChild(h3); return card; }
innerHTML vs createElement
innerHTML
createElement
innerHTML = "..."
const filtered = allSpots.filter((row) => { const genres = splitGenres(row.category || ""); return genres.includes(filterGenre); // 条件に一致するものだけ残す });
filter は条件に一致する要素だけを残した新しい配列を返す (元の allSpots は変わらない)
allSpots
// 例 [1, 2, 3, 4, 5].filter((n) => n > 3); // → [4, 5]
ここで触っているのは、観光スポット一覧を出す前の下ごしらえです。
[6-1] normalizeSpaces
logNormalizeSpacesTry()
[6-2] splitGenres
category
"歴史, 自然"
logSplitGenresTry()
[]
[6-3] collectUniqueGenres
logCollectUniqueGenresTry()
parsedRows
ここから、実際にスポット一覧と絞り込み UI を画面に出す処理に入ります。
[6-4] spotCardFromRow
logSpotCardFromRowTry()
row
{ name, category, url, description }
[6-5] renderSpots
filterGenre
"__all__"
splitGenres
[6-6] populateGenreSelect
genres
[6-7] loadSpotsData
棒グラフで数値は見えるようになった
でも…「この日は行っていいの?ダメなの?」が一瞬でわからない
→ 混雑日は赤、空き日は緑で色分けして、 直感的に判断できるようにしたい
const CROWD_THRESHOLD = 500; // 人数の目安 function barColor(nPeople) { if (nPeople <= 0) return COLORS.unknown; // 未確定 → グレー if (nPeople < CROWD_THRESHOLD) return COLORS.empty; // 空きやすい → 緑 return COLORS.crowded; // 混みやすい → 赤 }
条件分岐(if)で値を変える
if
→ これを全日分 map して backgroundColor の配列にする
backgroundColor
function renderTopThree(rows) { const positive = rows.filter((r) => r.n_people > 0); // 予約人数が 0 より大きい日 const sorted = [...positive].sort((a, b) => a.n_people - b.n_people); // 少ない順 const top = sorted.slice(0, 3); // 先頭 3 件 for (const r of top) { const li = document.createElement("li"); li.textContent = `${r.date_visit} — 予約 ${r.n_people.toLocaleString("ja-JP")} 人`; list.appendChild(li); } }
filter → sort → slice の組み合わせで 「予約が少ない順 TOP 3」を取り出す
sort
slice
ここで触っているのは、グラフを見て終わりにせず「おすすめ日」を文章で出す処理です。
[7-1] barColor
logBarColorTry()
nPeople
COLORS.unknown
CROWD_THRESHOLD
COLORS.empty
COLORS.crowded
[7-2] renderTopThree
[4-2]
{ date_visit, n_people, ... }
CSV テキスト ↓ split / map(手書きパース)または Papa Parse オブジェクト配列 ↓ map(ラベル・数値・色を取り出す) Chart.js に渡す配列 ↓ Chart.js 棒グラフ(色分け付き) スポット CSV ↓ Papa Parse オブジェクト配列 ↓ filter(ジャンル絞り込み) ↓ createElement(DOM 構築) スポットカード一覧
でも、天気 はこの CSV には含まれていない。 「予約が少ない日」と「行きやすい天気」を一緒に考えたい → 別のソース が必要
text()
json()
どちらも ブラウザからは同じ fetch。 「データを公開している」という点ではオープンデータと近く、受け取り方が CSV か JSON か が違う、と考えるとつながる。
発展のヒント(例):
// 発展のイメージ(メインの完成版では TOP 3 に未使用) function weatherScore(precipitation) { if (precipitation === 0) return 30; if (precipitation < 5) return 10; return -20; } const score = -r.n_people + (precip !== null ? weatherScore(precip) : 0);
「1つのデータを見る」から、 「複数のデータを組み合わせて判断する」 へ
const URL = "https://api.open-meteo.com/v1/forecast" + "?latitude=36.08&longitude=136.51" + "&daily=precipitation_sum" + "&timezone=Asia%2FTokyo&forecast_days=16"; const res = await fetch(URL); const json = await res.json(); const dates = json.daily.time; // ["2026-04-13", "2026-04-14", ...] const precips = json.daily.precipitation_sum; // [0.0, 2.3, 0.0, ...]
latitude
longitude
daily=precipitation_sum
forecast_days=16
open-meteo.html
daily
file://
http://localhost
loadWeatherData()
renderTopThree
// 降水量(mm)が少ないほど加点(メインの完成版では TOP 3 に未使用) function weatherScore(precipitation) { if (precipitation === 0) return 30; if (precipitation < 5) return 10; return -20; } const score = -r.n_people + (precip !== null ? weatherScore(precip) : 0);
予約データの date_visit をキーに降水量を Map で照合する、という組み合わせ方の例
Map
ここで触っているのは、Open-Meteo から降水量を取って画面に出す処理です(おすすめ日の並びは引き続き予約人数のみ)。
[8-1] weatherScore
logWeatherScoreTry()
precipitation
30
10
-20
[8-2] loadWeatherData
weather-status
取得した天気(今後 5 日)
OPEN_METEO_URL
await res.json()
.json()
async/await
localStorage
map.html
attribution
<!-- map.html の head / body 末尾のイメージ --> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <div id="map" style="height: 400px;"></div>
// 福井県周辺を中心に地図を作る(教材の初期ズーム例) const map = L.map("map").setView([36.06, 136.22], 10); L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', }).addTo(map);
setView([緯度, 経度], ズームレベル)
L.tileLayer(...)
// fukui-spot を読み込んだ配列の各スポットにマーカーを立てる for (const spot of allSpots) { L.marker([Number(spot.lat), Number(spot.lng)]) .addTo(map) .bindPopup(`<b>${spot.name}</b><br>${spot.category}`); }
L.marker([lat, lng])
.bindPopup(html)
Number(spot.lat)
fukui-spot の CSV には lat lng 列が含まれている
lat
lng
改善タイムで 何をしてみたか を、グループ内で短く共有 する時間です。
デモ:ブラウザで画面を見せられるなら一言添える(無理なら説明だけで OK)
プログラミングの技術は「課題を解くための道具」として、 必要になったタイミングで登場した
オープンデータを1つ選び、それを表示する Web アプリを作成してください。
Web アプリの作成 HTML・CSS・JavaScript を使って、選んだデータを表示するアプリを作ってください。 表示形式は問いません(グラフ・表・一覧・地図など何でも可)。
レポートの作成 以下の3点を別途レポートにまとめてください。
配布資料 handout.md も合わせて活用してください
handout.md
発表後に、必要な人だけ見れば OK
template/app.js
initMap()
renderMarkers()
applyMapFilter()
leaflet.css
leaflet.js
labelFromPrecip()
barColorForPrecip()
buildOrUpdatePrecipChart()
loadOpenMeteoDemo()
Chart.js
Marp は Mermaid を描画しないため、HTML でフローを表示