MyGreatCircle is a web application that visualizes personal location history as beautiful great circle arcs on a world map. Users enter a list of places they've lived, worked, or visited, and instantly receive stunning visualizations showing their life journey connected by geodesic paths across the globe.
The application requires no account, no database, and generates all outputs instantly in the browser. The name references the great circle paths that connect places on a sphere—the shortest distance between two points on Earth, rendered as elegant curved arcs.
As a user, I want to enter a list of places I've lived or visited so that I can visualize my life journey.
Acceptance Criteria:
- A text area accepts multiple lines, one place per line
- Places can optionally include year or year range (e.g., "London 1990-1995")
- Places are geocoded automatically upon submission
- Ambiguous places show alternatives for user selection
- Original text is preserved if user wants to edit the list
As a user, I want to see my places connected by great circle arcs so that I can appreciate my life's geographic journey visually.
Acceptance Criteria:
- Map displays with selected visual theme
- Great circles connect places in sequence
- Place markers are visible at each location
- Four themes available: Minimal Dark, Vibrant Neon, Vintage, Clean Modern
- Theme switching is instant without re-rendering data
- Map is responsive on mobile and desktop
As a user, I want to see statistics about my journey so that I can understand my travel patterns.
Acceptance Criteria:
- Total places count displayed
- Unique countries count displayed
- Total journey distance in kilometers
- Longest single move highlighted with place names
- Optional: time-based statistics if dates are provided
- Statistics update when theme changes or places are refined
As a user, I want to download a PDF fact sheet so that I can share or print my journey summary.
Acceptance Criteria:
- A4 portrait PDF generated
- Contains map visualization, stats, and place list
- Subtle watermark "mygreatcircle.com" in footer
- Downloads immediately without account
- PDF is vector-based for crisp printing
As a user, I want to download a print-ready poster so that I can display my journey on a wall.
Acceptance Criteria:
- A3 landscape PDF generated
- Full-bleed map visualization as hero element
- Place sequence displayed in footer
- Watermark "mygreatcircle.com" in corner
- Downloads immediately without account
- PDF is vector-based for crisp printing
As a user, I want to edit my place list after seeing the visualization so that I can refine incorrect geocoding or add missing places.
Acceptance Criteria:
- "Edit List" button returns to input state with text preserved
- Original text formatting is maintained
- User can modify, add, or remove places
- Re-submission triggers fresh geocoding
As a user, I want to select correct alternatives when a place name is ambiguous so that my visualization is accurate.
Acceptance Criteria:
- Low-confidence geocodes show a warning indicator
- Clicking a place reveals alternative matching locations
- User can select the correct match
- Selection updates the visualization immediately
As a user, I want to see the environmental impact of my journey so that I can understand my carbon footprint and optionally take action to offset it.
Acceptance Criteria:
- A toggle (🌱 icon) in the InsightsPanel enables/disables eco impact display
- When enabled, shows a forest grid visualization with tree icons
- Each tree icon represents one tree's yearly CO2 absorption capacity (~21kg CO2)
- CO2 is calculated based on estimated transport mode (car <100km, train 100-800km, flight >800km)
- A link to One Tree Planted offset service is provided
- Eco stats appear in PDF exports when enabled
- Feature persists in localStorage with other journey data
The geocoding system uses a 3-tier caching strategy to minimize Nominatim API calls and share results across users:
-
Tier 1: Browser localStorage (exact match)
- First lookup, instant response
- Persists across browser sessions
- Versioned to allow cache invalidation
-
Tier 2: Server SQLite cache (fuzzy match)
- Batch lookup via
POST /api/places/lookup - Uses Levenshtein distance (max 3) for typo tolerance
- Prefix-based candidate filtering for performance
- Shared across all users
- Batch lookup via
-
Tier 3: Nominatim API (client-side, rate-limited)
- Direct browser-to-Nominatim requests
- Rate limited to 1 request per 1.1 seconds
- Results submitted back to server for shared caching
- User enters places
- Check localStorage for exact matches
- Batch lookup unresolved places from server (
POST /api/places/lookup) - Query Nominatim directly for remaining places (sequential, rate-limited)
- Cache results in localStorage
- Submit newly resolved places to server (
POST /api/places/submit)
Nominatim and OpenStreetMap attribution must be displayed in:
- Web UI footer (both compact and full versions)
- PDF fact sheet footer
- PDF poster footer
- README documentation
Attribution text: "Geocoding by Nominatim · Map data © OpenStreetMap contributors"
- Visualization rendered with D3.js
- PDF generation with jsPDF + svg2pdf.js
- No server-side processing for PDFs
- All computation happens in the browser
- Generate unique IDs using UUID for place tracking
- No user accounts required for MVP
- No database for MVP (future enhancement)
- Session data lives only in browser
- All state managed client-side with React hooks
- Shareable URLs possible via future enhancement
- Layout works on mobile, tablet, and desktop
- Header pinned to top, footer to bottom
- Main content scrolls or pages as needed
- Touch-friendly interface for mobile
- Page load and initial interaction < 1 second
- Geocoding requests batched where possible
- Cache hits should return instantly
- PDF generation completes in < 5 seconds
- Semantic HTML structure
- Color contrast meets WCAG AA standards
- Keyboard navigation support
- Alt text for all meaningful images
interface Place {
id: string; // Generated UUID
rawInput: string; // Original user text
name: string; // Parsed place name
yearStart?: number; // Optional start year
yearEnd?: number; // Optional end year
coordinates: [number, number]; // [longitude, latitude]
confidence: 'high' | 'low'; // Geocoding confidence
alternatives?: GeocodingResult[];// Other possible matches
}
interface Journey {
places: Place[];
theme: 'minimal' | 'neon' | 'vintage' | 'modern';
}
interface Insights {
totalPlaces: number;
countries: string[];
totalDistanceKm: number;
longestLegKm: number;
longestLegFrom: string;
longestLegTo: string;
yearsSpanned?: number;
longestStay?: { place: string; years: number };
}
interface GeocodingResult {
name: string;
lat: number;
lng: number;
confidence: 'high' | 'low';
boundingbox?: [number, number, number, number];
}
interface EcoStats {
totalCO2Kg: number; // Total journey CO2 emissions in kg
treeCount: number; // Trees needed to offset
legs: LegEmission[]; // Per-leg breakdown
}
interface LegEmission {
from: string;
to: string;
distanceKm: number;
mode: 'car' | 'train' | 'flight';
co2Kg: number;
}GET /api/health
Response:
{
"status": "ok"
}
Batch fuzzy lookup of places from the server-side cache.
POST /api/places/lookup
Request:
{
"queries": ["Cape Town", "Johannesburg", "Durban"]
}
Response:
{
"resolved": {
"cape town": {
"displayName": "Cape Town, Western Cape, South Africa",
"lat": -33.9249,
"lng": 18.4241
},
"johannesburg": {
"displayName": "Johannesburg, Gauteng, South Africa",
"lat": -26.2041,
"lng": 28.0473
}
},
"unresolved": ["durban"]
}
Submit newly geocoded places to the shared server cache.
POST /api/places/submit
Request:
{
"places": [
{
"query": "Durban",
"displayName": "Durban, KwaZulu-Natal, South Africa",
"lat": -29.8587,
"lng": 31.0218,
"importance": 0.75
}
]
}
Response:
{
"saved": 1
}
Reserved for future implementation under /api/v1/:
- User accounts and journey persistence
- Journey sharing with short URLs
- Analytics and usage tracking
┌─────────────────────────────────────────────────────────────────┐
│ Browser (Client) │
├─────────────────────────────────────────────────────────────────┤
│ React + Chakra UI │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Place Input │→ │ Map Preview │→ │ PDF Generation │ │
│ │ (textarea) │ │ (D3.js SVG) │ │ (jsPDF + svg2pdf.js) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
│ ↓ ↓ │
│ Input Parser D3 Visualization SVG → PDF │
│ Place matching Theme rendering Client-side │
└─────────────────────────────────────────────────────────────────┘
│
▼ /api/geocode
┌─────────────────────────────────────────────────────────────────┐
│ Go Backend │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Static File │ │ Geocode │ │ Future API │ │
│ │ Server │ │ Proxy+Cache │ │ Endpoints │ │
│ │ (Vite dist) │ │ (Nominatim) │ │ (accounts, sharing) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼ (rate-limited, cached)
┌─────────────┐
│ Nominatim │
│ (external) │
└─────────────┘
- Stateless (no database for MVP)
- No user accounts
- Client-side rendering and PDF generation
- Backend is thin: static files + geocoding proxy with caching
MyGreatCircle/
├── .github/
│ └── workflows/
│ ├── test.yml # Go tests + lint on PR
│ ├── build.yml # Multi-platform build verification
│ └── release.yml # Tagged releases → packages
├── cmd/
│ └── mygreatcircle/
│ └── main.go # Entry point, CLI flags
├── internal/
│ ├── api/
│ │ ├── server.go # HTTP server setup, routing
│ │ ├── handlers.go # Health check endpoint
│ │ ├── places.go # Places lookup/submit endpoints
│ │ └── places_test.go # Places API tests
│ └── db/
│ ├── repository.go # PlaceRepository interface
│ ├── sqlite.go # SQLite implementation with fuzzy matching
│ └── db_test.go # Database tests
├── web/
│ ├── src/
│ │ ├── components/
│ │ │ ├── PlaceInput.jsx # Text input with parser
│ │ │ ├── MapVisualization.jsx # D3.js visualization
│ │ │ ├── ThemeSelector.jsx # Theme toggle
│ │ │ ├── PlaceList.jsx # Place list with alternatives
│ │ │ ├── InsightsPanel.jsx # Journey statistics
│ │ │ ├── OutputCards.jsx # PDF download cards
│ │ │ ├── EmailModal.jsx # Email capture for poster
│ │ │ ├── TreeGrid.jsx # Forest grid eco visualization
│ │ │ └── Footer.jsx # Branding footer
│ │ ├── hooks/
│ │ │ ├── useGeocoding.js # Geocoding API integration
│ │ │ └── usePdfGeneration.js # PDF generation logic
│ │ ├── lib/
│ │ │ ├── parser.js # Parse place input text
│ │ │ ├── themes.js # Visual theme definitions
│ │ │ ├── geo.js # Great circle math, distance, insights
│ │ │ ├── carbon.js # CO2 emissions calculations
│ │ │ ├── carbon.test.js # Carbon calculation tests
│ │ │ ├── nominatim.js # Client-side Nominatim with rate limiting
│ │ │ ├── nominatim.test.js # Nominatim module tests
│ │ │ ├── wordcloud.js # Word cloud positioning algorithm
│ │ │ ├── wordcloud.test.js # Word cloud tests
│ │ │ └── topo/world.json # World map TopoJSON
│ │ ├── App.jsx # Main app component, state management
│ │ └── main.jsx # React entry point
│ ├── public/
│ │ └── favicon.ico
│ ├── dist/ # Built frontend (Vite)
│ ├── index.html
│ ├── package.json
│ ├── package-lock.json
│ └── vite.config.js
├── docs/
│ ├── mkdocs.yml # MkDocs configuration
│ └── docs/
│ ├── index.md # User guide
│ ├── developer.md # Developer guide
│ ├── architecture.md # Architecture details
│ └── api.md # API documentation
├── scripts/
│ └── build-packages.sh # Release packaging script
├── flake.nix # Nix dev environment
├── flake.lock
├── Makefile # Development tasks
├── .pre-commit-config.yaml # Code quality checks
├── .exrc # Neovim key mappings
├── .nvim.lua # Neovim config
├── SPECIFICATION.md # This file
├── PACKAGES.md # Package documentation
├── README.md # Project overview
└── go.mod # Go module definition
- Projection: Equal Earth (
d3.geoEqualEarth()) - Great circles:
d3.geoInterpolate()for geodesic paths - Rendering hierarchy:
- Background (theme-specific gradient or solid)
- Land masses (subtle, de-emphasized)
- Great circle arcs (hero element, prominent)
- Place markers (dots at each location)
- Labels (optional, theme-dependent)
| Theme | Background | Land | Arcs | Points |
|---|---|---|---|---|
| Minimal Dark | #1a1a2e → #16213e |
rgba(255,255,255,0.05) |
White, 1px, 60% opacity | White dots |
| Vibrant Neon | #0f0f23 |
rgba(255,255,255,0.08) |
Rainbow gradient + glow filter | Colored + glow |
| Vintage | #f4f1ea (paper) |
#e8e0d0 with stroke |
Sepia dashed #5c4033 |
Terracotta #b85c38 |
| Clean Modern | #ffffff → #f8fafc |
#e2e8f0 |
Solid blue #3b82f6 |
Blue dots |
- Library: jsPDF + svg2pdf.js
- Execution: Client-side (browser)
- Output: Vector PDFs (crisp at any print size)
┌────────────────────────────────────────┐
│ MyGreatCircle │
│ "Your Life in Places" │
├────────────────────────────────────────┤
│ Paris │
│ [Map Visualization] Tokyo │
│ (40% of page height) │
│ London Berlin │
├────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ │
│ │ X Places │ │ Y Countries │ │
│ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ N km total │ │ M km longest │ │
│ └──────────────┘ └──────────────┘ │
├────────────────────────────────────────┤
│ Made with ♥ by Kartoza │
│ Geocoding by Nominatim · © OSM │
│ mygreatcircle.com │
└────────────────────────────────────────┘
Place names appear as a word cloud in the margins around the map (gray text, equal size, random placement) rather than a bullet list.
┌──────────────────────────────────────────────────────────────┐
│ │
│ │
│ [Full-bleed Map Visualization] │
│ (Great circles as hero, 85% of area) │
│ │
│ │
├──────────────────────────────────────────────────────────────┤
│ Place 1 → Place 2 → Place 3 → ... mygreatcircle.com│
└──────────────────────────────────────────────────────────────┘
Provides reproducible development environment with:
- Go 1.22+, Node.js 22, golangci-lint, gopls
nix developenters the dev environment- All dependencies managed via Nix
make dev- Start backend + frontend dev serversmake build- Production binary with embedded web assetsmake test- Run Go testsmake lint- Run lintersmake web-dev- Frontend dev server onlymake web-build- Build frontend for production
- Go:
go fmt,go vet,golangci-lint - JavaScript/JSX:
prettier,eslint - General: spell check, license headers
- test.yml: Runs on PRs, Go tests + lint
- build.yml: Multi-platform build verification
- release.yml: On version tags, builds packages for DEB, RPM, Flatpak
.exrcwith leader+p shortcuts for common tasks.nvim.luawith Go/JavaScript language settings- which-key bindings for navigation
- User can enter places and see visualization in < 30 seconds
- All four themes render correctly and look distinctive
- PDFs generate client-side without server involvement
- Geocoding handles ambiguous places gracefully
- Responsive design works on mobile and desktop
- Great circles render correctly using proper geodesics
- Insights calculations are accurate
- Performance is snappy (< 1s first load, < 5s PDF generation)
- Persistence: Optional accounts, saved journeys
- Sharing: Short URLs (mygreatcircle.com/v/abc123)
- Premium Features: Additional themes, no watermarks, exclusive outputs
- Physical Products: Print-on-demand merchandise
- Self-hosted Nominatim: If rate limits become an issue
- Analytics: Anonymous usage insights
- Social Sharing: Preset templates for common journey patterns
Footer on all pages and outputs: