Live demo: library-final-five.vercel.app
An interactive dashboard for exploring, comparing, and choosing among Seoul's 215 public libraries — from a city-wide map view down to a single library's real-time seat availability, opening hours, and book collection.
The project combines a lightweight data pipeline (collects and cleans public-data.go.kr API responses + reference datasets) with a React/TypeScript single-page app that visualizes the result through a linked map, ranking list, cross-filter panel, scatter-plot explorer, and per-library detail view.
Finding a library to study at in Seoul usually means checking several disconnected sources: a map for location, the library's own site for hours, and a separate portal for real-time seat congestion. This project brings all of that into a single, linked view so a user can:
- Compare districts/libraries at a glance — see relative congestion, open seats, hours, and collection size across the whole city on a map.
- Rank libraries by personal priorities — weight criteria such as congestion, open seats, closing time, and book collection (optionally by genre) to get a personalized ranking.
- Drill into a single library — view its weekly occupancy heatmap, today's hours, live reading-room seat counts, and its book collection (including genre breakdown and yearly acquisition trends).
Map exploration, ranking/filtering, and single-library detail are designed as linked views: a selection made in one view (map, list, heatmap, scatter plot) is reflected in the others.
library_visualization/
├── analysis/ # Data collection & processing scripts
│ ├── scripts/
│ │ ├── fetch-seoul-library-api.mjs # Pulls live data from data.go.kr
│ │ ├── fetch_prst_data.py # (reference) operating-status fetch
│ │ ├── fetch_public_data.py # (reference) public dataset fetch
│ │ └── generate_heatmap.py # Builds the processed JSON files
│ ├── notebooks/ # Exploratory analysis (placeholder)
│ └── outputs/ # Analysis artifacts (placeholder)
│
├── data/
│ ├── raw/api/ # Raw API snapshots (JSON/XML)
│ │ ├── info_v2/ # 자치단체 공공도서관 통합정보
│ │ ├── prst_info_v2/ # 운영현황 정보
│ │ └── rlt_rdrm_info_v2/ # 열람실 실시간 정보
│ ├── processed/ # Cleaned, merged output
│ │ ├── enriched_libraries.json # 215 libraries, merged + enriched
│ │ └── real_time_heatmap.json # Realtime seat snapshot
│ ├── reference/ # Reference/lookup data
│ ├── 서울시 공공도서관 현황정보.csv # Base library registry (215 libs)
│ └── 공공도서관 통계데이터_*.xlsx # Genre-level collection statistics
│
└── web/ # React + TypeScript + Vite application
├── public/data/ # Static copies of processed data served to the app
└── src/
├── App.tsx # Top-level state & layout
├── components/ # UI components (see §6)
├── utils/
│ ├── score.ts # Recommendation scoring
│ └── libNameMatch.ts # Cross-source library name matching
├── types/library.ts # Shared TypeScript types
└── styles/global.css # All styling
| Source | Description | Used for |
|---|---|---|
data.go.kr B551982/plr_v2 — info_v2 |
자치단체 공공도서관 통합정보 (basic info: address, hours, contact, coordinates) | Library metadata |
data.go.kr B551982/plr_v2 — prst_info_v2 |
자치단체 공공도서관 운영현황 정보 (operating status, reservation availability) | Filters (reservation, laptop room) |
data.go.kr B551982/plr_v2 — rlt_rdrm_info_v2 |
자치단체 공공도서관 열람실 실시간 정보 (live reading-room occupancy) | Real-time congestion / seat availability, day-of-week heatmap |
| 서울시 공공도서관 현황정보.csv | Base registry of all 215 Seoul public libraries | Canonical library list & coordinates |
| 공공도서관 통계데이터 (xlsx) | Genre-level collection statistics (10 KDC top-level categories) | Genre bar/bubble charts, genre-weighted recommendation |
| newbook-data.json | Yearly new-acquisition counts (2019–2024) per library | Collection modal trend chart |
The web app never calls these APIs directly. All collection and merging
happens offline via the scripts in analysis/scripts/, producing static JSON
files that the frontend fetches.
data.go.kr (B551982/plr_v2)
│
│ node analysis/scripts/fetch-seoul-library-api.mjs
▼
data/raw/api/{info_v2, prst_info_v2, rlt_rdrm_info_v2}/latest.{json,xml,meta.json}
│
│ python3 analysis/scripts/generate_heatmap.py
│ ── merges with ──
│ data/서울시 공공도서관 현황정보.csv (215 library registry)
│ data/공공도서관 통계데이터_*.xlsx (genre statistics)
▼
data/processed/
├── enriched_libraries.json (215 libraries, merged + enriched)
└── real_time_heatmap.json (per-library day×hour occupancy)
│
│ cp → web/public/data/processed/
▼
React app ── fetch('/data/processed/enriched_libraries.json') ──▶ UI
To refresh the dataset with the latest live data:
# 1. Fetch the latest snapshot from data.go.kr
node analysis/scripts/fetch-seoul-library-api.mjs
# 2. Merge it with the reference CSV/XLSX into processed JSON
python3 analysis/scripts/generate_heatmap.py
# 3. Copy the processed output into the web app's public folder
cp data/processed/enriched_libraries.json web/public/data/processed/enriched_libraries.json
cp data/processed/real_time_heatmap.json web/public/data/processed/real_time_heatmap.jsonEach library receives a score between 0 and 1, computed as a weighted sum of up to four criteria. Users adjust each weight (0–10) with sliders in the sidebar; the relative weights determine each criterion's contribution.
| Criterion | Weight key | Value (0–1) | Notes |
|---|---|---|---|
| Congestion | congestion |
1 − congestionRate |
0.5 if no real-time data available |
| Open seats | seats |
(total − used) / total reading-room seats |
0.5 if no real-time data |
| Closing time | lateHours |
Linear scale: 18:00 → 0, 23:00 → 1 | 0.5 if hours unknown |
| Book collection | bookCount |
genreCount / 10,000 (if a genre is selected) or bookCount / 100,000 (overall), capped at 1 |
Genre is selectable in the sidebar |
score = Σ ( value_i × weight_i / Σ weight )
If all weights are 0, every library receives a neutral score of 0.5.
Scores drive the map's color scale, the library list ranking, and the
Tradeoff Explorer scatter plot.
The app is a single "Live Recommendation" view built around shared state in
App.tsx. A selection in any view updates the others:
┌────────────────────────────┐
│ App.tsx │
│ weights · filters · district│
│ selectedLibrary · compareLibs│
└───────────────┬──────────────┘
┌───────────────┬────────────────┼───────────────┬─────────────────┐
▼ ▼ ▼ ▼ ▼
RecommendSidebar RecommendMap CrossFilterPanel DowHeatmap LibraryList
(score weights, (Leaflet map, + LibraryUnitGrid (day × hour (ranked list,
district/genre/ colored by + TradeoffChart occupancy tags, "compare"
laptop filters) score) (multi-metric heatmap, click selection)
filtering & to select a
scatter plot) library)
│ │
▼ ▼
LibraryDetail RadarCompare
(hours · seats · (5-axis radar for
collection · up to 2 selected
genre chart) libraries)
│
▼
CollectionModal
(book-count comparison,
genre bubble chart,
yearly acquisition trend)
| File | Description |
|---|---|
App.tsx |
Top-level state (libraries, weights, filters, selection), data loading, and layout composition. |
main.tsx |
React entry point. |
types/library.ts |
Shared EnrichedLibrary / ReadingRoom types matching enriched_libraries.json. |
| File | Description |
|---|---|
RecommendSidebar.tsx |
Score-weight sliders, genre selector, district dropdown, and live-filter checkboxes. |
RecommendMap.tsx |
Leaflet map of all libraries, colored by score, with district drill-down and location-based filters. |
CrossFilterPanel.tsx |
Multi-metric range filters (congestion, seats, hours, books, travel time) shared with the scatter plot. |
LibraryUnitGrid.tsx |
Compact grid of library "unit cells" reflecting cross-filter results. |
TradeoffChart.tsx |
Scatter plot ("Tradeoff Explorer") for comparing any two metrics across libraries, highlighting cross-filter matches. |
DowHeatmap.tsx |
Day-of-week × hour occupancy heatmap (city-wide, per-district, or per-library), built with d3. |
LibraryList.tsx |
Ranked list of libraries with quick-glance tags (seats, hours, laptop room, reservation, top genre) and a "compare" toggle. |
RadarCompare.tsx |
5-axis radar chart comparing up to two selected libraries (Availability, Open Seats, Hours, Collection, Capacity). |
LibraryDetail.tsx |
Detail panel for the selected library: info, hours, seat layout, and collection summary. |
HoursViz.tsx |
Weekly opening-hours bar visualization. |
SeatUnitViz.tsx |
Reading-room seat availability visualization. |
GenreChart.tsx |
Per-library genre bar chart with hover tooltip and Seoul-average marker, used inside the Collection modal. |
GenrePieChart.tsx |
Top-3-genre donut chart (+ "Others") shown at a glance in the Collection card. |
CollectionModal.tsx |
Modal with total-book comparison (library vs. district vs. Seoul average), full genre bar chart, and yearly new-acquisition trend. |
| File | Description |
|---|---|
utils/score.ts |
computeScore() — weighted recommendation scoring; scoreToColor() — score → map color; getDistrictGroups() — per-district aggregation. |
utils/libNameMatch.ts |
Resolves library-name discrepancies between the realtime heatmap dataset and enriched_libraries.json (static mapping + normalized fuzzy matching). |
- Node.js 18+
- Python 3.9+ (for the data pipeline)
# Install dependencies
npm --prefix web install
# Add your data.go.kr API key
echo "VITE_DATA_GO_KR_SERVICE_KEY=<your-key>" > .envnpm run dev # start the Vite dev server
npm run build # production build
npm run preview # preview the production buildnpm run fetch:api # fetch latest API snapshot
python3 analysis/scripts/generate_heatmap.py # rebuild processed JSON- Frontend: React 18, TypeScript, Vite
- Mapping: Leaflet / react-leaflet
- Charts: Recharts (bar/line/radar/scatter), d3 (heatmap, bubble chart)
- Data pipeline: Node.js (API fetch), Python + pandas (merging/enrichment)
- Deployment: Vercel