Skip to content

Commit 01d93e0

Browse files
committed
Revamp webpage structure and styling for "Is Europe Falling Behind?" including enhanced controls, updated chart layout, and improved accessibility. Replace TypeScript with JavaScript, and update dependencies in package files. Remove unused files and streamline data processing in Python script.
1 parent 6d42c72 commit 01d93e0

22 files changed

Lines changed: 7563 additions & 3063 deletions

.dockerignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Keeps local-only and generated files out of Docker build contexts.
2+
node_modules
3+
dist
4+
.git
5+
.DS_Store
6+
npm-debug.log*
7+
coverage

.npmrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Keep dependency versions reproducible and avoid very new npm releases.
2+
save-exact=true
3+
min-release-age=7

Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Defines the containerized Node.js environment for running the Vite app without
2+
# installing project dependencies directly on the host machine.
3+
FROM node:24-alpine
4+
5+
WORKDIR /app
6+
7+
# Run installation and the dev server as the unprivileged node user.
8+
RUN chown node:node /app
9+
USER node
10+
11+
COPY --chown=node:node package*.json .npmrc ./
12+
RUN npm install --no-audit --no-fund
13+
14+
COPY --chown=node:node . .
15+
16+
EXPOSE 5173
17+
18+
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

Milestone 3.pdf

480 KB
Binary file not shown.

docker-compose.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Runs the local Vite development server in Docker while keeping dependencies
2+
# inside an isolated container volume.
3+
services:
4+
web:
5+
build:
6+
context: .
7+
command: sh -c "npm install --no-audit --no-fund && npm run dev -- --host 0.0.0.0"
8+
user: node
9+
ports:
10+
- "5173:5173"
11+
volumes:
12+
- .:/app
13+
- node_modules:/app/node_modules
14+
15+
volumes:
16+
node_modules:

index.html

Lines changed: 95 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,106 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
<title>Special Operations: Country Growth Profiles</title>
6+
<link rel="preconnect" href="https://fonts.googleapis.com" />
7+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
8+
<link
9+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Instrument+Serif:ital@0;1&display=swap"
10+
rel="stylesheet"
11+
/>
12+
<title>Is Europe Falling Behind?</title>
713
</head>
814
<body>
915
<div id="app">
10-
<header>
11-
<h1>Country Growth Profiles</h1>
12-
<p>Growth relative to <strong>year 2000</strong> baseline &mdash; select a country and drag the year slider</p>
16+
<header class="page-header">
17+
<p class="eyebrow">COM&middot;480 &nbsp;&middot;&nbsp; Data Visualization</p>
18+
<h1>
19+
Is Europe <em>really</em> falling behind?
20+
</h1>
21+
<p class="lede">
22+
Choose an economic indicator, animate it through time, and compare a
23+
handful of countries across the metrics that matter.
24+
</p>
1325
</header>
1426

15-
<div id="controls">
16-
<div class="control-group">
17-
<label for="country-select">Country</label>
18-
<select id="country-select"></select>
19-
</div>
20-
<div class="control-group">
21-
<label for="year-slider">Year: <span id="year-label">2024</span></label>
22-
<input type="range" id="year-slider" min="2000" max="2024" value="2024" step="1" />
23-
</div>
24-
</div>
25-
26-
<div id="chart-container">
27-
<div id="spider-chart"></div>
28-
<div id="legend"></div>
29-
</div>
27+
<main class="mvp-layout">
28+
<section class="map-panel" aria-label="Interactive map">
29+
<div class="control-bar">
30+
<div class="control-group control-group--inputs">
31+
<label class="control-field">
32+
<span class="control-label">Indicator</span>
33+
<select id="indicator-select"></select>
34+
</label>
35+
36+
<label class="control-field control-field--year">
37+
<span class="control-label">
38+
Year <span id="year-label" class="control-value">2023</span>
39+
</span>
40+
<input id="year-slider" type="range" min="2000" max="2023" value="2023" step="1" />
41+
</label>
42+
43+
<button id="play-button" class="play-button" type="button" aria-label="Play timeline">
44+
<span class="play-button__icon" aria-hidden="true"></span>
45+
<span class="play-button__label">Play</span>
46+
</button>
47+
</div>
48+
49+
<div class="control-group control-group--toggles">
50+
<div class="segmented-control" aria-label="Visualization mode">
51+
<button id="map-mode" type="button" data-active="true">Map</button>
52+
<button id="scatter-mode" type="button">Scatter</button>
53+
</div>
54+
55+
<div class="segmented-control" aria-label="Selection mode">
56+
<button id="country-mode" type="button" data-active="true">Countries</button>
57+
<button id="region-mode" type="button">Regions</button>
58+
</div>
59+
60+
<div class="segmented-control" aria-label="Value mode">
61+
<button id="growth-mode" type="button" data-active="true">Growth</button>
62+
<button id="absolute-mode" type="button">Absolute</button>
63+
</div>
64+
65+
<button id="toggle-spider" class="ghost-button" type="button" aria-pressed="false">Show spider</button>
66+
</div>
67+
</div>
68+
69+
<div id="map-chart" class="map-chart"></div>
70+
71+
<aside id="spider-panel" class="spider-overlay" aria-label="Spider comparison" hidden>
72+
<div class="spider-heading">
73+
<div>
74+
<p class="eyebrow">Spider graph</p>
75+
<h2>Profile comparison</h2>
76+
</div>
77+
<button id="close-spider" type="button" aria-label="Close spider graph">Close</button>
78+
</div>
79+
<p id="spider-caption">Click countries on the map to compare their full normalized profiles.</p>
80+
<div id="spider-chart" class="chart"></div>
81+
</aside>
82+
83+
<section class="evolution-panel" aria-label="Evolution chart">
84+
<div>
85+
<p class="eyebrow">Evolution</p>
86+
<h2 id="evolution-title">Indicator trajectory</h2>
87+
</div>
88+
<div id="evolution-chart" class="chart"></div>
89+
</section>
90+
91+
<div class="selection-strip">
92+
<p id="selected-summary"></p>
93+
<button id="reset-selection" type="button">Reset selection</button>
94+
</div>
95+
</section>
96+
</main>
97+
98+
<footer>
99+
<p>
100+
Growth mode normalizes every country to its year-2000 baseline.
101+
Absolute mode reveals economic scale where the underlying units are
102+
comparable; ETF prices remain a normalized public-market proxy.
103+
</p>
104+
</footer>
30105
</div>
31-
<script type="module" src="./src/main.ts"></script>
106+
<script type="module" src="./src/main.js"></script>
32107
</body>
33108
</html>

milestone2/preprocess.py

Lines changed: 100 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@
33
"""
44

55
import json
6-
import os
6+
from pathlib import Path
77
import sys
88

99
import pandas as pd
10-
import yfinance as yf
1110

12-
DATA_DIR = "data"
11+
try:
12+
import yfinance as yf
13+
except ImportError:
14+
yf = None
15+
16+
ROOT_DIR = Path(__file__).resolve().parent.parent
17+
DATA_DIR = ROOT_DIR / "data"
18+
PUBLIC_DIR = ROOT_DIR / "public"
1319
BASELINE_YEAR = 2000
1420
LATEST_YEAR = 2023
1521

@@ -40,14 +46,65 @@
4046
"USA": ("hist", "SPY"),
4147
}
4248

49+
# Region metadata used by the final narrative site.
50+
REGION_BY_COUNTRY: dict[str, str] = {
51+
"AUS": "Asia-Pacific",
52+
"CAN": "North America",
53+
"SWE": "Europe",
54+
"DEU": "Europe",
55+
"HKG": "Asia-Pacific",
56+
"ITA": "Europe",
57+
"JPN": "Asia-Pacific",
58+
"BEL": "Europe",
59+
"CHE": "Europe",
60+
"MYS": "Asia-Pacific",
61+
"NLD": "Europe",
62+
"AUT": "Europe",
63+
"ESP": "Europe",
64+
"FRA": "Europe",
65+
"SGP": "Asia-Pacific",
66+
"GBR": "Europe",
67+
"MEX": "North America",
68+
"KOR": "Asia-Pacific",
69+
"BRA": "Latin America",
70+
"USA": "North America",
71+
}
72+
73+
METRIC_METADATA: dict[str, dict[str, str | bool]] = {
74+
"GDP": {
75+
"unit": "current US$",
76+
"absoluteLabel": "Economic weight",
77+
"aggregate": "sum",
78+
"absoluteComparable": True,
79+
},
80+
"GDP per Capita": {
81+
"unit": "current US$ per person",
82+
"absoluteLabel": "Prosperity",
83+
"aggregate": "mean",
84+
"absoluteComparable": True,
85+
},
86+
"ETF Price": {
87+
"unit": "ETF share price, adjusted close",
88+
"absoluteLabel": "ETF price",
89+
"aggregate": "mean",
90+
"absoluteComparable": False,
91+
},
92+
"Market Cap": {
93+
"unit": "current US$",
94+
"absoluteLabel": "Listed company market value",
95+
"aggregate": "sum",
96+
"absoluteComparable": True,
97+
},
98+
}
99+
43100
WB_SKIPROWS = 4
44101
_yf_cache: dict[str, pd.DataFrame] = {}
45102

46103

47104
# ── Data helpers ──────────────────────────────────────────────────────────────
48105

49106
def load_wb(filename: str) -> pd.DataFrame:
50-
path = os.path.join(DATA_DIR, "worldbank", filename)
107+
path = DATA_DIR / "worldbank" / filename
51108
df = pd.read_csv(path, skiprows=WB_SKIPROWS, index_col="Country Code")
52109
for col in df.columns:
53110
if str(col).strip().isdigit():
@@ -59,6 +116,10 @@ def _get_yf_supplement(symbol: str) -> pd.DataFrame:
59116
"""Download 2020-2024 data via yfinance (cached). Returns df with 'close' column."""
60117
if symbol in _yf_cache:
61118
return _yf_cache[symbol]
119+
if yf is None:
120+
print(f" yfinance unavailable for {symbol}; using local ETF history only.", file=sys.stderr)
121+
_yf_cache[symbol] = pd.DataFrame()
122+
return _yf_cache[symbol]
62123
try:
63124
ticker = yf.Ticker(symbol)
64125
hist = ticker.history(start="2019-12-01", end="2024-06-30", auto_adjust=True)
@@ -80,11 +141,11 @@ def load_etf_data(source: str, symbol: str) -> pd.DataFrame:
80141
"""Load ETF data from local file, supplement with yfinance for post-2020 years.
81142
Returns a DataFrame with a 'close' column, indexed by date (ascending)."""
82143
if source == "etf":
83-
path = os.path.join(DATA_DIR, "nasdaq", "etf", f"{symbol}.csv")
144+
path = DATA_DIR / "nasdaq" / "etf" / f"{symbol}.csv"
84145
df = pd.read_csv(path, index_col="Date", parse_dates=True)
85146
df = df.rename(columns={"Close": "close"})
86147
elif source == "hist":
87-
path = os.path.join(DATA_DIR, "stock", "history", f"{symbol}.csv")
148+
path = DATA_DIR / "stock" / "history" / f"{symbol}.csv"
88149
df = pd.read_csv(path, index_col="date", parse_dates=True)
89150
df = df.rename(columns={"close": "close"})
90151
df = df.sort_index()
@@ -166,12 +227,29 @@ def normalize_series(raw: dict, years: list[int], base: float) -> list:
166227
]
167228

168229

230+
def absolute_series(raw: dict, years: list[int]) -> list:
231+
return [
232+
round(raw[y], 4) if raw.get(y) is not None else None
233+
for y in years
234+
]
235+
236+
237+
def make_metric_series(raw: dict, years: list[int], base: float, metric: str) -> dict:
238+
metadata = METRIC_METADATA[metric]
239+
return {
240+
"normalized": normalize_series(raw, years, base),
241+
"absolute": absolute_series(raw, years),
242+
"unit": metadata["unit"],
243+
"absoluteComparable": metadata["absoluteComparable"],
244+
}
245+
246+
169247
# ── Main ──────────────────────────────────────────────────────────────────────
170248

171249
def main() -> None:
172250
print("Loading World Bank data...")
173251
iso_codes = pd.read_csv(
174-
os.path.join(DATA_DIR, "iso", "countries.csv"), index_col="alpha-3"
252+
DATA_DIR / "iso" / "countries.csv", index_col="alpha-3"
175253
)
176254
gdp = load_wb("gdp-current-usd-2026.csv")
177255
gdp_pc = load_wb("gdp-capita-current-usd-2026.csv")
@@ -212,9 +290,11 @@ def main() -> None:
212290
etf_raw = {y: get_annual_etf_price(etf_df, y) for y in years}
213291

214292
timeseries: dict = {
215-
"GDP": normalize_series(gdp_series, years, gdp_base),
216-
"GDP per Capita": normalize_series(gdp_pc_series, years, gdp_pc_base),
217-
"ETF Price": normalize_series(etf_raw, years, etf_base),
293+
"GDP": make_metric_series(gdp_series, years, gdp_base, "GDP"),
294+
"GDP per Capita": make_metric_series(
295+
gdp_pc_series, years, gdp_pc_base, "GDP per Capita"
296+
),
297+
"ETF Price": make_metric_series(etf_raw, years, etf_base, "ETF Price"),
218298
}
219299

220300
# Market cap (optional; excluded for CHN, IND, SWE, RUS)
@@ -223,7 +303,9 @@ def main() -> None:
223303
mc_base = mc_series.get(BASELINE_YEAR) if mc_series else None
224304
if mc_series and mc_base and mc_base > 0:
225305
mc_filled, n_filled = fill_mc_gaps(mc_series, etf_df, years)
226-
timeseries["Market Cap"] = normalize_series(mc_filled, years, mc_base)
306+
timeseries["Market Cap"] = make_metric_series(
307+
mc_filled, years, mc_base, "Market Cap"
308+
)
227309
suffix = f"MC ok ({n_filled} years ETF-proxy filled)"
228310
else:
229311
suffix = "MC skipped: no 2000 baseline"
@@ -235,6 +317,7 @@ def main() -> None:
235317
)
236318
countries_out[iso3] = {
237319
"name": str(country_name),
320+
"region": REGION_BY_COUNTRY[iso3],
238321
"etf": symbol,
239322
"timeseries": timeseries,
240323
}
@@ -250,14 +333,17 @@ def main() -> None:
250333
"latestYear": LATEST_YEAR,
251334
"years": years,
252335
"axes": universal_axes,
336+
"metricMetadata": METRIC_METADATA,
337+
"regions": sorted(set(REGION_BY_COUNTRY.values())),
253338
"mcExcluded": sorted(MC_EXCLUDE),
254339
"countries": countries_out,
255340
}
256341

257-
os.makedirs("../public", exist_ok=True)
258-
output_path = "../public/spider_data.json"
342+
PUBLIC_DIR.mkdir(exist_ok=True)
343+
output_path = PUBLIC_DIR / "spider_data.json"
259344
with open(output_path, "w") as f:
260-
json.dump(output, f, separators=(",", ":"))
345+
json.dump(output, f, indent=2)
346+
f.write("\n")
261347

262348
# summary
263349
print(f"\n Wrote {len(countries_out)} countries to {output_path}")

0 commit comments

Comments
 (0)