Top world news stories on an interactive world map — automatically ingested, AI-summarised, geo-tagged, and displayed as positioned callouts
Live: newschart.rossarnold.uk
Each major AI model has quietly become a news editor - deciding, from everything happening in the world, which stories matter most right now. NewsChart makes that editorial judgement visible and comparable. Switch between Gemini, Perplexity, and ChatGPT to see which stories each AI is leading with, where in the world their attention is focused, and how their picks diverge. The New York Times provides a human editorial baseline for comparison.
The result is part news reader, part AI observatory: a way to watch - across different days and different models - whose version of the world you're being shown.
Features:
- Daily news from four sources: Gemini, Perplexity, ChatGPT, and NYT RSS
- AI-powered summarisation and geo-tagging via Google Gemini and OpenRouter models
- Interactive Mercator / Natural Earth world map with up to 3 story callouts
- Time travel — browse historical news by date (desktop slider or mobile chip strip)
- Callout layout algorithm that minimises overlaps and connector crossings (based on PFLP literature)
- Dark map theme with frosted-glass callout boxes
- Keyboard navigation (left/right arrow keys on timeline)
- Observability: Prometheus metrics, Grafana dashboards
How it works: AI models, prompts, and engineering →
- Multi-model AI comparison — three independent LLMs (each with native web search) plus a human-edited NYT baseline, queried daily via the same prompt so differences in coverage are the models' own.
- Typed AI output — Spring AI's structured-output binding maps model responses directly to
NewsHighlightJava records; no free-form text parsing anywhere in the pipeline. - Full-stack type safety — Java 21 records on the backend, TypeScript on the frontend, shared schema via REST contract.
- Production-grade CI/CD — split backend/frontend pipelines, Testcontainers integration tests against real MongoDB, OWASP Dependency-Check + Sonatype OSS Index + Dependabot across Maven/npm/Actions, tag-triggered deploys via Tailscale.
- PFLP callout layout algorithm — exhaustive candidate enumeration (16 directions × 6 distances per callout) with penalty scoring for overlaps, connector crossings, connectors passing through boxes, and viewport violations. Based on Christensen, Marks & Shieber (1995); feasible because N ≤ 3.
- Java 21+
- Node.js 20+ and npm
- MongoDB (via Docker Compose, or a local install)
- Google Gemini API key (for AI summarisation)
- OpenRouter API key (optional, for Perplexity/OpenAI sources)
./setup.shThis configures pre-push checks (security audit + accessibility tests) that mirror CI.
docker compose up -d mongodbexport GOOGLE_API_KEY=your_gemini_key
export OPENROUTER_API_KEY=your_openrouter_key # optional, for Perplexity/OpenAI
./mvnw spring-boot:runThe backend starts on port 8080 and begins ingesting news on a schedule.
cd frontend
npm install
npm start # dev server on :3000, proxies API calls to :8080Open http://localhost:3000.
NewsChart is a full-stack application with a Java Spring Boot backend and React frontend.
Scheduler → Pipeline Orchestrator → News Source
→ parse / summarise / geo-tag via AI
→ MongoDB (Callout)
→ REST API (/api/news/calloutsForDay/{date})
→ React frontend
→ PFLP callout layout algorithm
→ Interactive world map
| Package | Description |
|---|---|
api/ |
REST controllers — GET /api/news/calloutsForDay/{date}, GET /api/news/availableDays |
news/source/ |
NYT RSS ingestion and parsing |
news/highlights/ |
Processed highlights — NewsHighlights, CalloutBuilderService |
news/pipeline/ |
Modular pipeline: BasePipelineOrchestrator, NYT, Gemini, OpenRouter variants |
callout/ |
Callout entity, repository, service, source/type enums |
scheduler/ |
Background fetch scheduling |
ai/ |
GeminiGatewayService, OpenRouterGatewayService, AiPrompts |
geo/ |
Country entity and CountryFactory |
Pipeline sources:
| Source | Model | Notes |
|---|---|---|
| Gemini | gemini-2.5-flash |
Google Search grounding enabled |
| Perplexity | perplexity/sonar-pro-search |
Via OpenRouter, native web search |
| OpenAI | openai/gpt-4o-search-preview |
Via OpenRouter, native web search |
| NYT RSS | gemini-2.5-flash-lite (2.5, geo-tag only) |
Human-curated feed; Gemini summarises and geo-tags, search grounding off |
| File | Description |
|---|---|
MapChart.tsx |
Main map component (Mercator projection via react-simple-maps) |
StoryCalloutList.tsx |
Renders callout boxes with SVG connectors to country markers |
DateTimeline.tsx |
Date navigation (desktop slider + mobile chip strip) |
utils/mapCalloutUtils.ts |
Exhaustive PFLP-based callout layout algorithm |
Layout algorithm: Generates up to ~96 candidate positions per callout (16 directions × 6 distances), evaluates all combinations for up to N=3 callouts, and selects the lowest-penalty layout. Penalises overlaps, viewport violations, connector crossings, connectors passing through boxes, and connector length.
MongoDB stores two collections:
news_rss— raw RSS/AI news itemsnews_highlights— processed, summarised, geo-tagged highlights (source of callouts)
./mvnw test
./mvnw test -Dtest=ClassName#methodNameUses Testcontainers (MongoDB) for integration tests — requires Docker.
cd frontend
CI=true npm test # unit tests (React Testing Library)The callout layout algorithm has its own headless test harness that runs each fixture across 9 viewports × 2 projections and checks hard geometric constraints (no box overlap, no out-of-bounds, no connector crossings or connector-through-box, no obscured origins).
# Run all fixtures, print pass/fail summary, write test-output/layout-report.json
node scripts/run-layout-tests.mjs
# Filter by fixture id, tag, or group
node scripts/run-layout-tests.mjs --id handcrafted-wide-aus-bra-can
node scripts/run-layout-tests.mjs --tag needs-fix
node scripts/run-layout-tests.mjs --group handcrafted
# Single viewport or projection
node scripts/run-layout-tests.mjs --viewport laptop-1366-typ --projection mercator
# Capture screenshots (builds frontend, starts vite preview, saves PNGs)
node scripts/run-layout-tests.mjs --screenshots
node scripts/run-layout-tests.mjs --tag needs-fix --screenshotsScreenshots land in frontend/test-output/screenshots/ (gitignored). Filename convention: {id}__{projection}__{viewport}.png.
Fixture corpus — frontend/src/__tests__/layout/fixtures/
handcrafted-*.json— hand-authored cases (committed, run in CI vianpm test)live-*.json— captured from the production API viascripts/export-callouts.mjs(committed once they're interesting, then treated identically to handcrafted)
Tag conventions
| Tag | Meaning |
|---|---|
needs-fix |
Known algorithm failure — skipped in CI so the build stays green; kept as a regression target |
sample |
Transcription of an in-app ?testCase=N scenario |
tight-cluster |
Origins are very close together on the map |
vertical-stack |
Origins share similar longitudes, stacked vertically |
wide-spread |
Origins are widely separated — expected to always pass |
Adding a live fixture from the API
# Fetch from production (writes live-<date>-<provider>.json if ≥3 callouts)
node scripts/export-callouts.mjs \
--base-url https://newschart.rossarnold.uk \
--providers GOOGLE_GEMINI \
--date 2026-05-10
# Date range, multiple providers
node scripts/export-callouts.mjs \
--base-url https://newschart.rossarnold.uk \
--providers NEW_YORK_TIMES,GOOGLE_GEMINI \
--from 2026-05-01 --to 2026-05-10
# --force overwrites an existing fixture (use carefully — preserves no hand-edits)Eyeball the result, add tags/notes in the JSON, then commit. Re-running without --force will never clobber a committed fixture.
# Frontend production build
cd frontend && npm run build
# Backend fat jar (bundles built frontend from frontend/build/)
./mvnw packagedocker compose --profile monitoring up -dStarts Prometheus (:9091), Grafana (:3001), Node Exporter, and a Grafana dashboard watcher that syncs changes back to the repo. Dashboards are in grafana/dashboards/.
Deployment is triggered automatically by pushing a v* tag, or manually via GitHub Actions workflow_dispatch.
Pipeline:
- Build frontend (
npm run build) - Set Maven version from the git tag
- Build backend fat jar (
./mvnw package -DskipTests) - Connect to production host via Tailscale
rsyncjar and frontend build to production serversystemctl restart newschart
See .github/workflows/deploy-to-live.yml for details.
Built with:
- Spring Boot 4 — backend framework
- Google Gemini API — AI summarisation and geo-tagging
- OpenRouter — multi-model AI gateway (Perplexity, OpenAI, etc.)
- React 18 — frontend framework
- react-simple-maps — world map component
- world-atlas — TopoJSON world geometry
- MongoDB — news storage
- Testcontainers — integration test infrastructure
See full credits and licenses →
GPL-3.0-or-later — see LICENSE for details.