Rendez-vous with inspiration, s'il vous plaît!

L'argent ne fait pas le bonheur - French idiom.
There’s that sweet moment before every big thing, before the commitment. When nothing’s started yet and everything still shines with an if, or a maybe. You don’t know what’s coming, not really, but the road is there. All ahead of you. So you hit the highway of your own thoughts and confusion intertwines with freedom. Then, suddenly, there it is: crush! This one actually.
Demo:
This interactive data-story helps visualizing wellbeing across European countries from 2004 to 2024.
Built with TypeScript and D3.js. It has been crafted for and during the Hackaviz 2026 competition.
Contents - click to expand
- Synoptic visualization: each country is a wedge with a wedge radius that is an aggregate happiness index. Wedge segments encode color-differentiated socio-economic sub-indicators.
- Guidance: a multi-stage narrative auto-advances (once) and can be replayed with navigation arrows.
- Year scrubbing & auto-play roll through two decades manually, or let it play automatically.
- Country selection Pin a specific country and see its detail in the center readout.
- A cluster mode outlines each country by its economic cluster.
- Perception mode overlays an extra arc showing how perceived (subjective) happiness compares to the measured index.
- Screen-reader-friendly data table: accessible hidden, always-in-sync tabular equivalent of whatever year is on screen. For users who can't consume the radial chart visually.
- TypeScript compiled to native ES modules with
tsc(no bundler) - D3.js v7 loaded via CDN at runtime and typed at compile time via
@types/d3 - Plain CSS: custom properties, no preprocessor
- Zero runtime dependencies. The shipped app is static files. TypeScript/
@types/d3are dev-only, needed to build, not to run
The app itself is static files (index.html + compiled dist/*.js + css/ + data/) and needs no dependencies to run. Building it from the TypeScript source needs Node once, to compile.
Prerequisite:
Make sure Node.js (version 20+) is available on your system
Build and run:
npm install # installs typescript + @types/d3 (dev-only)
npm run build # compiles src/**.ts -> dist/**.js
npm run serve # serves the app on http://localhost:8080
# or, both at once:
npm startdist/ is generated and gitignored. Run npm run build after cloning, or after any edit to src/.
Tip
Use npm run watch to recompile automatically while developing.
Because the app uses native ES modules (<script type="module">) and fetches the dataset via fetch, it must be served over HTTP(S). Opening index.html directly from the filesystem (file://) will not work in most browsers.
A prebuilt dist/ is included in the release zip, so npm run serve alone (no build step) is enough if you just want to run it as-is.
Note: releases are created only when you push with a git tag:
git tag v1.0.0
git push --tagsA workflow is included at .github/workflows/deploy.yml.
- It installs dependencies, type-checks and compiles the TypeScript (
npm run build) - Assembles the static site (
index.html,css/,data/,dist/) and publishes it via GitHub's official Pages deployment action - Ships a release, so the app can be run as-is (avoids the
npmbuilding step).
No gh-pages branch, no personal access token, and no manual build/upload is required.
Prerequisite:
- In the repo's Settings → Pages.
- Under Build and deployment → Source, make sure "GitHub Actions" is selected (not "Deploy from a branch").
- Push to
main(or run the workflow manually from the Actions tab → Deploy to GitHub Pages → Run workflow).
The first push to main after that will trigger the workflow automatically. Once it finishes, the site is live at:
https://<username>.github.io/<repo-name>/
Why this approach instead of a gh-pages branch?
- GitHub's
actions/upload-pages-artifact+actions/deploy-pagesis the current recommended method: it deploys straight from the build artifact via OIDC, so there's no branch to keep in sync and no token to manage. - No config changes needed: all paths in
index.htmlare relative (css/styles.css,dist/main.js,data/final_wellbeing_dataset.csv), so the site works correctly whether it's served at a domain root or under a project subpath like the URL above.
If you specifically want the classic gh-pages branch approach instead (to also serve the site outside of GitHub Pages), in deploy.yaml swap the upload Pages artifact and actions/deploy-pages@v4 of the workflow for peaceiris/actions-gh-pages, pointed at the same _site folder.
wheel-of-wellbeing/
├── .github/
│ └── workflows/
│ └── deploy.yml GitHub Actions: build + deploy to GitHub Pages
├── index.html Entry point: markup, meta tags, a11y hooks (loads dist/main.js)
├── css/
│ └── styles.css All visual styling (tokens, layout, chart primitives, a11y utilities)
├── data/
│ └── final_wellbeing_dataset.csv
├── src/ TypeScript source (edit this)
│ ├── main.ts App entry point: boot sequence + control wiring
│ ├── config.ts Static config: stage copy, colors, dimensions, clusters
│ ├── state.ts Single shared mutable state object (+ AppState type)
│ ├── dom.ts Cached, precisely-typed DOM/d3-selection references
│ ├── dataService.ts CSV loading + parsing + year-indexing
│ ├── utils.ts Small shared helpers (formatting, motion, interaction gating)
│ ├── tooltip.ts Floating tooltip (content + positioning + auto-hide)
│ ├── legend.ts Main legend, cluster legend, perception legend
│ ├── story.ts Stage/narrative controller (year nav, stage transitions, auto-play)
│ ├── types.ts Shared domain types (CountryRow, Stage, ChartScales, ...)
│ ├── types/
│ │ └── global.d.ts Declares the runtime-global `d3` (loaded via CDN, not imported)
│ └── chart/
│ ├── setup.ts Builds the persistent SVG layers + glow filters (once)
│ ├── index.ts Per-year render orchestrator (computes shared scales)
│ ├── grid.ts Concentric reference rings
│ ├── clock.ts Center "clock" tick marks
│ ├── centerInfo.ts Animated year/country/score readout
│ ├── labels.ts Rotated country name labels
│ ├── arcs.ts Core stacked-arc renderer + interactions (the big one)
│ └── accessibility.ts Screen-reader data table + live status announcements
├── tsconfig.json Strict compiler config, targets native browser ESM
├── package.json Build/serve scripts + dev-only type dependencies
├── package-lock.json CI deterministic install
├── .editorconfig
├── .gitignore
├── CONTRIBUTING.md
├── CODE_OF_CONDUCT.md
├── README.md You are here Θ_Θ
└── LICENSE CC0 1.0
If you're new to the codebase, start at src/main.ts and follow the imports. Every module in src/ has a short JSDoc header explaining its responsibility.
Note
Edit files in src/, never in dist/!
dist/ is fully regenerated by tsc and any manual changes there will be silently overwritten on the next build.
main.tsbuilds the static SVG layer structure (chart/setup.ts), loads and indexes the CSV (dataService.ts), wires up every button/toggle, then kicks off the intro stage and a one-time auto-advance schedule.story.tsowns when: which of the four stages is active, which year is on screen, and the transitions between them.chart/index.tsowns what gets drawn: given a year's rows, it computes the shared angle/radius scales once and delegates to the grid, clock, labels, and arc sub-renderers.chart/arcs.tsis the heart of the visual: for each country it builds a stack of multiple sub-indicator segments, plus optional cluster-outline and perception-overlay arcs. It also owns all hover/click/keyboard interaction.state.tsis the single source of truth for anything mutable (current year, selected country, active modes, etc.), typed via theAppStateinterface. Every other module reads/writes it directly rather than passing state through long call chains.
data/final_wellbeing_dataset.csv contains one row per country per year (2004-2024) with these important features:
Happiness_Index: the aggregate wellbeing score driving each wedge's radiusSubjective_Indexis a self-reported/perceived happiness (used by perception mode)- Ten socio-economic sub-indicators (income, employment, housing, work-life balance, health, environment, social connections, civic engagement, safety, education) that make up each wedge's stacked segments.
A methodology note is available.
This refactor deliberately applies practices from several disciplines. Named explicitly, as requested:
Contents - click to expand
Type safety
-
Strict TypeScript (
strict: true) across the entire codebase: every function signature, DOM/d3 selection and data shape is typed. This catches a whole class of bugs (typos in property names, wrong argument order,undefinednot handled) at compile time instead of at runtime in the browser. -
Domain types as documentation:
types.tsdefines only onceCountryRow,Stage,ClusterDefinition,ChartScales, etc. Every module imports and reuses them instead of relying on implicit object shapes. -
Precisely-typed DOM/d3 selections:
dom.tstypes each cached selection with its real element type (e.g.Selection<HTMLInputElement, ...>for a checkbox), so calling the wrong method on the wrong element is a compile error, not a silent runtime no-op. -
Type-only imports for the global
d3:d3is still loaded via CDN at runtime (not bundled), but is fully typed at compile time via@types/d3. Type imports are erased during compilation so they add zero runtime cost or extra requests.
Code organization & readability
-
DRY (Don't Repeat Yourself): repeated logics (interaction gating, opacity dimming, tooltip HTML building, per-year data filtering) are a single function called from every site that needs it.
-
Separation of Concerns / Single Responsibility Principle: one file, one job (data loading vs. rendering vs. narrative logic vs. DOM caching).
-
Modular architecture: native ES modules with explicit imports/exports make dependencies traceable instead of implicit via shared closure scope.
-
Named constants over magic numbers: for example, stage indices (
STAGE.DASHBOARDinstead of the raw3, typed as theStageIndexunion) and other repeated literals are named once inconfig.ts. -
Consistent naming & JSDoc: every module and non-trivial function has a doc comment explaining why, not just what.
-
Configuration/data separated from logic: all static content (stage copy, colors, dimensions, cluster definitions) lives in
config.ts. -
.editorconfigis available for consistent indentation/line-endings across editors and contributors.
Performance
-
Pre-indexed data:
dataByYearis built once at load time, turning every year lookup into an O(1)Map.get. -
Event listeners bound once: hover/click/keyboard handlers are attached to the D3
enterselection only, not re-bound on every render. -
Shared scales, computed once per render: the angle scale, radius scale, and arc generator are computed once per year and passed down.
-
Cheaper tooltip updates:
mousemoveonly repositions the existing tooltip. Content is only rebuilt onmouseenter. -
prefers-reduced-motionsupport: respects the OS-level motion preference, which also reduces unnecessary animation work for users who've opted out of it.
Accessibility (WCAG-oriented)
-
Text alternative for the chart: a visually-hidden, always-in-sync data table (
#a11yTable) gives screen-reader users the same ranking/values a sighted user sees in the radial chart (WCAG 1.1.1 / 1.3.1). -
Live region announcements: a concise
aria-live="polite"status announces year changes without forcing the whole table to be read out. -
Keyboard interaction: countries are focusable (
tabindex,role="button") and selectable via Enter/Space, not just mouse click. -
Accessible names for controls: toggle switches are linked to their visible label text via
aria-labelledbyand exposerole="switch"/aria-checked. The play/pause button exposesaria-pressedand the legend toggle exposesaria-expanded. -
Visible focus states:
:focus-visibleoutlines added for buttons, toggles, and country wedges. -
prefers-reduced-motionsee above. This is also an accessibility feature (vestibular disorders, motion sensitivity). -
SVG semantics:
role="img"with a<title>/<desc>pair describes the chart's purpose. -
Consistent language:
<html lang="fr">. The UI copy is predominantly French, story navigationaria-labels translated for consistency.
SEO
-
Descriptive
<title>and<meta name="description">. -
Open Graph / Twitter card metadata.
-
application/ld+jsonstructured data (WebApplication). -
Favicon (inline SVG data URI, no extra file/request).
-
<noscript>fallback message. -
rel="preconnect"to the D3 CDN.
Maintainability & repo hygiene
-
package.jsonwithbuild/watch/serve/startscripts and dev-only type dependencies (typescript,@types/d3). -
tsconfig.jsonin strict mode, configured to emit native browser ES modules (no bundler required). -
.gitignore(excludesnode_modules/and the generateddist/) and.editorconfig.
The radial chart is a rich visual encoding with no fully equivalent screen-reader experience on its own. Rather than attempt to make every hover/animation detail accessible, this refactor focuses on equivalent access to the underlying information:
- A hidden data table (reachable via a screen reader's table-navigation commands) lists every visible country, its rank, its happiness index, and its subjective index for the currently displayed year.
- A short live-region announcement fires on year change.
- Every country wedge is keyboard-focusable and selectable.
- All interactive controls (buttons, toggles) have accessible names and appropriate ARIA roles/states.
This is a solid baseline, not a claim of full WCAG AA conformance, see Known limitations.
Targets evergreen browsers (Chrome, Firefox, Safari, Edge — last 2 versions) via native ES modules, CSS custom properties, and backdrop-filter. No transpilation or polyfills are included; if you need to support older browsers, introduce a build step (e.g. esbuild/Vite) and a CSS fallback for backdrop-filter.
-
D3 via CDN, not a bundled package. This keeps the app dependency-free at runtime (TypeScript/
@types/d3are dev-only, used purely for compiling and type-checking) but also means the app is offline-unfriendly and pinned to whateverd3.v7.min.jscurrently resolves to. -
A few type positions (the arc generator's zero-argument call, in particular) needed a small
as unknown as DefaultArcObjectcast to bridge d3's datum-oriented typings with this app's constant-setter usage pattern. This is documented inline inchart/arcs.ts. -
noUncheckedIndexedAccessis intentionally off. The ten secondary CSV columns are accessed dynamically viarow[key]. Turning this stricter flag on would require non-null assertions or guards at every such access for limited real-world benefit here. Worth reconsidering if the dynamic-column pattern is refactored into named properties. -
backdrop-filteris used on several stacked panels which is a known performance cost (GPU compositing) on some lower-end devices. Preserved as-is since it's core to the visual design. Worth profiling if targeting low-end hardware. -
Full keyboard/screen-reader parity for the radial chart itself (as opposed to the equivalent data table) would require a much larger accessibility investment (custom roving-tabindex grid, sonification, etc.) and is out of scope here.
-
No i18n system. All copy is hardcoded French. Internationalizing would mean extracting strings into a translation layer.
See contributing file:
- Fork/clone the repo.
npm install(installstypescript+@types/d3, dev-only).npm run watchto recompile automatically as you edit, andnpm run serve(in another terminal) to preview.- Make your change in the relevant module under
src/(see project structure).- Most changes touch exactly one file.
- Never edit
dist/directly.
npm run buildand confirmtscreports zero errors before committing.- If you touch
src/chart/arcs.tsorsrc/story.ts, manually re-check: stage navigation, year scrubbing/auto-play, country selection, cluster mode, perception mode. They can break easily.
The Toulouse dataviz Meetup. Thanks to their team for organizing the Hackaviz 2026 Edition.
Ben Scott for the radial inspiration.
CC0 1.0 Universal. Inspire yourself.
Ꙭ