Skip to content

Commit 85873dd

Browse files
authored
refactor: tighten data boundaries (#14)
* refactor: tighten data boundaries * ci: deploy pages from master * docs: make readme more personal
1 parent d9ab859 commit 85873dd

10 files changed

Lines changed: 134 additions & 81 deletions

File tree

.github/workflows/deploy-pages.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Deploy to GitHub Pages
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [master]
66
workflow_dispatch:
77

88
permissions:

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
# Olympics Visualization
22

3-
A modern, interactive data visualization dashboard for Summer Olympics historical data (1896 - 2012). The app is now a fully TypeScript React/Vite project with high-performance D3.js visualizations, shared state, responsive layouts, and runtime CSV/JSON data loading.
3+
An interactive dashboard for exploring Summer Olympics data from 1896 to 2012.
44

55
This visualization was initially developed as a project for the Masters Course Information Visualization at Lisbon Técnico but turned into sort of a hobby of mine.
66

7+
The project has changed quite a bit since the first version. It is now a 100% TypeScript app built with React, Vite, and D3.js, with the data loaded from CSV/JSON files at runtime. The goal is still simple: make it easy to move between countries, years, sports, and medal breakdowns without losing the bigger picture.
8+
79
## Features
810

9-
- **Interactive World Map:** Custom D3.js Mercator projection with zoom/pan and country selection.
10-
- **Dynamic Bubble Chart:** Force-directed simulation to explore medals by Sport, Discipline, and Event.
11-
- **Comparative Trends:** Line charts and scatter plots with synchronized multi-country selection (up to 4).
12-
- **Stacked Medal Bars:** Gold, silver, and bronze composition for selected countries across the active year and sport filters.
13-
- **Population Efficiency:** Medal output normalized by average population, shown as medals per million people.
14-
- **Reorderable Dashboard:** Toggle reorder mode to move and resize visualizations in a responsive grid.
15-
- **Modern UI:** Built with Material UI and styled using the elegant Catppuccin color palette.
16-
- **Responsive Layout:** Dynamic grid system powered by `react-grid-layout`.
17-
- **Asynchronous Data:** High-performance runtime fetching of CSV/JSON datasets.
11+
- **World map:** Select countries directly from a D3-rendered map with zoom and pan support.
12+
- **Bubble chart:** Explore medals by sport, discipline, and event through a force-based view.
13+
- **Line and scatter charts:** Compare selected countries across years and medal totals.
14+
- **Stacked medal bars:** See gold, silver, and bronze composition for the active country, year, and sport filters.
15+
- **Population efficiency:** Compare medal output normalized by average population.
16+
- **Responsive dashboard:** Move and resize visualizations through a grid layout built for desktop and smaller screens.
17+
- **Runtime data loading:** Load the Olympics, population, and world map datasets from public CSV/JSON files.
1818

1919
## Tech Stack
2020

@@ -25,7 +25,8 @@ This visualization was initially developed as a project for the Masters Course I
2525
- **UI Components:** [Material UI](https://mui.com/)
2626
- **Grid Layout:** [React Grid Layout](https://github.com/STRML/react-grid-layout)
2727
- **Styling:** [Tailwind CSS](https://tailwindcss.com/), component CSS, and [Catppuccin](https://catppuccin.com/)
28-
- **Deployment:** GitHub Pages via GitHub Actions, with custom domain support
28+
- **Tooling:** Bun and npm scripts for local development, testing, and production builds
29+
- **Deployment:** GitHub Pages through GitHub Actions
2930

3031
## Getting Started
3132

@@ -37,24 +38,28 @@ This visualization was initially developed as a project for the Masters Course I
3738
### Installation
3839

3940
1. Clone the repository:
41+
4042
```bash
4143
git clone https://github.com/ayydany/Olympics-Visualization.git
4244
cd Olympics-Visualization
4345
```
4446

4547
2. Install dependencies:
48+
4649
```bash
4750
npm install
4851
```
4952

5053
3. Start the development server:
54+
5155
```bash
5256
npm run dev
5357
```
5458

5559
### Building for Production
5660

5761
To create an optimized production build:
62+
5863
```bash
5964
npm run build
6065
```
@@ -69,4 +74,4 @@ bun run build
6974
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
7075

7176
---
72-
Made with ❤️ by [ayydany](https://ayydany.com)
77+
Made with love by [ayydany](https://ayydany.com)

src/features/dashboard/components/Bubblechart.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ const Bubblechart: React.FC<BubblechartProps> = ({ dictionaryData, countryData,
4949
setDimensions({ width, height });
5050
});
5151

52-
resizeObserver.observe(container.parentElement!);
52+
if (container.parentElement) {
53+
resizeObserver.observe(container.parentElement);
54+
}
5355

5456
return () => resizeObserver.disconnect();
5557
}, [isResizing]);
@@ -246,7 +248,7 @@ const Bubblechart: React.FC<BubblechartProps> = ({ dictionaryData, countryData,
246248
.attr("stroke", "#cdd6f4");
247249
})
248250
.on("mousemove", (event) => {
249-
setTooltipState((prev: any) => ({
251+
setTooltipState((prev) => ({
250252
...prev,
251253
x: event.pageX,
252254
y: event.pageY

src/features/dashboard/components/Dashboard.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import Worldmap from "@/features/dashboard/components/Worldmap";
2424
import Tooltip from "@/components/Tooltip";
2525
import { fetchData } from "@/utils/api";
2626
import useYearStore from "@/stores/useYearStore";
27-
import { OlympicRow, DictionaryEntry, TooltipState } from "@/types";
27+
import { OlympicRow, DictionaryEntry, PopulationRow, TooltipState, WorldGeo } from "@/types";
2828

2929
import "react-grid-layout/css/styles.css";
3030
import "react-resizable/css/styles.css";
@@ -35,8 +35,8 @@ const MainComponent: React.FC = () => {
3535
const [gridWidth, setGridWidth] = useState(1200);
3636
const [dictionaryData, setDictionaryData] = useState<DictionaryEntry[] | null>(null);
3737
const [countryData, setCountyData] = useState<OlympicRow[] | null>(null);
38-
const [populationData, setPopulationData] = useState<any[] | null>(null);
39-
const [worldGeo, setWorldGeo] = useState<any>(null);
38+
const [populationData, setPopulationData] = useState<PopulationRow[] | null>(null);
39+
const [worldGeo, setWorldGeo] = useState<WorldGeo | null>(null);
4040
const [isLoading, setIsLoading] = useState(true);
4141
const [isResizing, setIsResizing] = useState(false);
4242
const [isReorderMode, setIsReorderMode] = useState(false);
@@ -108,8 +108,11 @@ const MainComponent: React.FC = () => {
108108
}
109109
}, [countrySelection.length, dictionaryData, setDefaultCountries]);
110110

111-
const visReady = useMemo(() => {
112-
return dictionaryData && countryData && populationData && worldGeo;
111+
const readyData = useMemo(() => {
112+
if (!dictionaryData || !countryData || !populationData || !worldGeo) {
113+
return null;
114+
}
115+
return { dictionaryData, countryData, populationData, worldGeo };
113116
}, [dictionaryData, countryData, populationData, worldGeo]);
114117

115118
const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
@@ -280,7 +283,7 @@ const MainComponent: React.FC = () => {
280283

281284
<div ref={gridContainerRef} className="flex-grow overflow-y-auto bg-ctp-crust p-2 relative">
282285
{isLoading && <div className="p-3 text-ctp-subtext0 font-medium text-center mt-10">Loading dataset...</div>}
283-
{visReady && (
286+
{readyData && (
284287
<ResponsiveGridLayout
285288
className="layout"
286289
layouts={filteredLayouts}
@@ -296,42 +299,42 @@ const MainComponent: React.FC = () => {
296299
{visibleCharts.worldmap && (
297300
<div key="worldmap" className={`vis-cell group ${isReorderMode ? "is-reordering" : ""}`}>
298301
<div className="viz-content w-full h-full pointer-events-auto">
299-
<Worldmap dictionaryData={dictionaryData!} setTooltipState={updateTooltipState} worldGeo={worldGeo!} isResizing={isResizing} />
302+
<Worldmap dictionaryData={readyData.dictionaryData} setTooltipState={updateTooltipState} worldGeo={readyData.worldGeo} isResizing={isResizing} />
300303
</div>
301304
</div>
302305
)}
303306
{visibleCharts.bubblechart && (
304307
<div key="bubblechart" className={`vis-cell group ${isReorderMode ? "is-reordering" : ""}`}>
305308
<div className="viz-content w-full h-full pointer-events-auto">
306-
<Bubblechart countryData={countryData!} dictionaryData={dictionaryData!} setTooltipState={updateTooltipState} isResizing={isResizing} />
309+
<Bubblechart countryData={readyData.countryData} dictionaryData={readyData.dictionaryData} setTooltipState={updateTooltipState} isResizing={isResizing} />
307310
</div>
308311
</div>
309312
)}
310313
{visibleCharts.scatterplot && (
311314
<div key="scatterplot" className={`vis-cell group ${isReorderMode ? "is-reordering" : ""}`}>
312315
<div className="viz-content w-full h-full pointer-events-auto">
313-
<Scatterplot countryData={countryData!} populationData={populationData!} dictionaryData={dictionaryData!} setTooltipState={updateTooltipState} isResizing={isResizing} />
316+
<Scatterplot countryData={readyData.countryData} populationData={readyData.populationData} dictionaryData={readyData.dictionaryData} setTooltipState={updateTooltipState} isResizing={isResizing} />
314317
</div>
315318
</div>
316319
)}
317320
{visibleCharts.linechart && (
318321
<div key="linechart" className={`vis-cell group ${isReorderMode ? "is-reordering" : ""}`}>
319322
<div className="viz-content w-full h-full pointer-events-auto">
320-
<Linechart countryData={countryData!} dictionaryData={dictionaryData!} setTooltipState={updateTooltipState} isResizing={isResizing} />
323+
<Linechart countryData={readyData.countryData} dictionaryData={readyData.dictionaryData} setTooltipState={updateTooltipState} isResizing={isResizing} />
321324
</div>
322325
</div>
323326
)}
324327
{visibleCharts.stackedMedals && (
325328
<div key="stackedMedals" className={`vis-cell group ${isReorderMode ? "is-reordering" : ""}`}>
326329
<div className="viz-content w-full h-full pointer-events-auto">
327-
<StackedMedalBars countryData={countryData!} dictionaryData={dictionaryData!} setTooltipState={updateTooltipState} isResizing={isResizing} />
330+
<StackedMedalBars countryData={readyData.countryData} dictionaryData={readyData.dictionaryData} setTooltipState={updateTooltipState} isResizing={isResizing} />
328331
</div>
329332
</div>
330333
)}
331334
{visibleCharts.populationEfficiency && (
332335
<div key="populationEfficiency" className={`vis-cell group ${isReorderMode ? "is-reordering" : ""}`}>
333336
<div className="viz-content w-full h-full pointer-events-auto">
334-
<PopulationEfficiencyChart countryData={countryData!} populationData={populationData!} dictionaryData={dictionaryData!} setTooltipState={updateTooltipState} isResizing={isResizing} />
337+
<PopulationEfficiencyChart countryData={readyData.countryData} populationData={readyData.populationData} dictionaryData={readyData.dictionaryData} setTooltipState={updateTooltipState} isResizing={isResizing} />
335338
</div>
336339
</div>
337340
)}

src/features/dashboard/components/PopulationEfficiencyChart.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import React, { useEffect, useMemo, useRef, useState } from "react";
22
import * as d3 from "d3";
33
import useYearStore from "@/stores/useYearStore";
4-
import { DictionaryEntry, OlympicRow, TooltipStateSetter } from "@/types";
4+
import { DictionaryEntry, OlympicRow, PopulationRow, TooltipStateSetter } from "@/types";
55
import "./PopulationEfficiencyChart.css";
66

7-
interface PopulationRow {
8-
CountryCode: string;
9-
[year: string]: string | number;
10-
}
11-
127
interface PopulationEfficiencyChartProps {
138
countryData: OlympicRow[];
149
populationData: PopulationRow[];

src/features/dashboard/components/Scatterplot.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import React, { useEffect, useRef, useState } from "react";
22
import * as d3 from "d3";
33
import useYearStore from "@/stores/useYearStore";
4-
import { OlympicRow, DictionaryEntry, TooltipStateSetter } from "@/types";
4+
import { OlympicRow, DictionaryEntry, PopulationRow, TooltipStateSetter } from "@/types";
55
import "./Scatterplot.css";
66

7-
interface PopulationRow {
8-
CountryCode: string;
9-
[year: string]: string | number;
10-
}
11-
127
interface ScatterPoint {
138
code: string;
149
name: string;
@@ -49,7 +44,9 @@ const Scatterplot: React.FC<ScatterplotProps> = ({ countryData, populationData,
4944
setDimensions({ width, height });
5045
});
5146

52-
resizeObserver.observe(container.parentElement!);
47+
if (container.parentElement) {
48+
resizeObserver.observe(container.parentElement);
49+
}
5350

5451
return () => resizeObserver.disconnect();
5552
}, [isResizing]);
@@ -243,7 +240,7 @@ const Scatterplot: React.FC<ScatterplotProps> = ({ countryData, populationData,
243240
.attr("stroke", "#cdd6f4");
244241
})
245242
.on("mousemove", (event) => {
246-
setTooltipState((prev: any) => ({
243+
setTooltipState((prev) => ({
247244
...prev,
248245
x: event.pageX,
249246
y: event.pageY

src/features/dashboard/components/Worldmap.tsx

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import React, { useEffect, useMemo, useRef, useState } from "react";
22
import * as d3 from "d3";
33
import useYearStore from "@/stores/useYearStore";
4-
import { DictionaryEntry, TooltipState } from "@/types";
4+
import { DictionaryEntry, TooltipStateSetter, WorldFeature, WorldGeo } from "@/types";
55
import "./Worldmap.css";
66

77
interface WorldmapProps {
88
dictionaryData: DictionaryEntry[];
9-
setTooltipState: (state: TooltipState | ((prev: TooltipState) => TooltipState)) => void;
10-
worldGeo: any;
9+
setTooltipState: TooltipStateSetter;
10+
worldGeo: WorldGeo;
1111
isResizing: boolean;
1212
}
1313

@@ -36,7 +36,9 @@ const Worldmap: React.FC<WorldmapProps> = ({ dictionaryData, setTooltipState, wo
3636
setDimensions({ width, height });
3737
});
3838

39-
resizeObserver.observe(container.parentElement!);
39+
if (container.parentElement) {
40+
resizeObserver.observe(container.parentElement);
41+
}
4042

4143
return () => resizeObserver.disconnect();
4244
}, [isResizing]);
@@ -53,7 +55,7 @@ const Worldmap: React.FC<WorldmapProps> = ({ dictionaryData, setTooltipState, wo
5355
.scale(dimensions.width / 6.2)
5456
.translate([dimensions.width / 2, dimensions.height / 1.5]);
5557

56-
const path = d3.geoPath().projection(projection);
58+
const path = d3.geoPath<WorldFeature>().projection(projection);
5759

5860
if (svg.select("defs").empty()) {
5961
const defs = svg.append("defs");
@@ -77,16 +79,14 @@ const Worldmap: React.FC<WorldmapProps> = ({ dictionaryData, setTooltipState, wo
7779

7880
svg.call(zoom);
7981

80-
const countries = g.selectAll<SVGPathElement, any>(".country")
81-
.data((worldGeo as any).features);
82-
83-
countries.enter()
84-
.append("path")
82+
g.selectAll<SVGPathElement, WorldFeature>(".country")
83+
.data(worldGeo.features)
84+
.join("path")
8585
.attr("class", "country")
86-
.merge(countries as any)
87-
.attr("d", path as any)
88-
.attr("fill", (d: any) => {
86+
.attr("d", path)
87+
.attr("fill", (d) => {
8988
const name = d.properties.name_long || d.properties.name;
89+
if (!name) return "url(#diagonalHatch)";
9090
const code = nameToCode[name];
9191
if (!code) return "url(#diagonalHatch)";
9292
if (countrySelection.includes(code)) {
@@ -96,16 +96,17 @@ const Worldmap: React.FC<WorldmapProps> = ({ dictionaryData, setTooltipState, wo
9696
})
9797
.attr("stroke", "#11111b")
9898
.attr("stroke-width", 0.5)
99-
.classed("country-selectable", (d: any) => {
99+
.classed("country-selectable", (d) => {
100100
const name = d.properties.name_long || d.properties.name;
101-
return !!nameToCode[name];
101+
return !!name && !!nameToCode[name];
102102
})
103-
.classed("country-unselectable", (d: any) => {
103+
.classed("country-unselectable", (d) => {
104104
const name = d.properties.name_long || d.properties.name;
105-
return !nameToCode[name];
105+
return !name || !nameToCode[name];
106106
})
107-
.on("mouseover", function(event, d: any) {
107+
.on("mouseover", function(event, d) {
108108
const name = d.properties.name_long || d.properties.name;
109+
if (!name) return;
109110
const code = nameToCode[name];
110111
if (!code) return;
111112

@@ -119,7 +120,7 @@ const Worldmap: React.FC<WorldmapProps> = ({ dictionaryData, setTooltipState, wo
119120
});
120121
})
121122
.on("mousemove", (event) => {
122-
setTooltipState((prev: any) => ({
123+
setTooltipState((prev) => ({
123124
...prev,
124125
x: event.pageX,
125126
y: event.pageY
@@ -129,8 +130,9 @@ const Worldmap: React.FC<WorldmapProps> = ({ dictionaryData, setTooltipState, wo
129130
d3.select(this).style("stroke", "#11111b").style("stroke-width", 0.5);
130131
setTooltipState({ show: false, content: "", x: 0, y: 0 });
131132
})
132-
.on("click", (event, d: any) => {
133+
.on("click", (event, d) => {
133134
const name = d.properties.name_long || d.properties.name;
135+
if (!name) return;
134136
const code = nameToCode[name];
135137
if (!code) return;
136138
toggleCountry(code, event.ctrlKey);

src/types/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type * as d3 from "d3";
2+
13
export type CountryCode = string;
24

35
export interface OlympicRow {
@@ -17,6 +19,32 @@ export interface DictionaryEntry {
1719
CountryCode: CountryCode;
1820
}
1921

22+
export interface PopulationRow {
23+
CountryCode: CountryCode;
24+
[year: string]: string | number;
25+
}
26+
27+
export interface WorldFeature {
28+
type: "Feature";
29+
properties: {
30+
name?: string;
31+
name_long?: string;
32+
};
33+
geometry: d3.GeoGeometryObjects;
34+
}
35+
36+
export interface WorldGeo {
37+
type: "FeatureCollection";
38+
features: WorldFeature[];
39+
}
40+
41+
export interface OlympicsData {
42+
dictionary: DictionaryEntry[];
43+
country: OlympicRow[];
44+
population: PopulationRow[];
45+
worldGeo: WorldGeo;
46+
}
47+
2048
export interface YearFilter {
2149
start: number;
2250
end: number;

0 commit comments

Comments
 (0)