Skip to content

smach/worldcup26

Repository files navigation

worldcup26

Get info about the 2026 FIFA Men's World Cup! See participating teams, schedule info, and past results. Ask natural language questions about the data on a Shiny dashboard! View all the info in an HTML page generated by Quarto!

Details

This package is an R wrapper for the football-data.org v4 API, focused on the 2026 FIFA Men’s World Cup. You can look up participating teams, as well as a team’s full schedule, its next match, and its past results.

It also includes a Shiny dashboard that uses the querychat R package to let you ask natural language questions about this data. It defaults to using Anthropic LLMs, requiring an Anthropic API key, but you can change the provider (instructions below).

The package repo also includes an associated Quarto website that uses Observable JavaScript to browse the data.

I had two goals for this project:

  • Use R to keep track of this year's men's World Cup tournament
  • See how well Claude could do in building what I wanted. Conclusion: Pretty well!!

Documentation below was mostly written by Claude (as was most of the code).

Installation

# install.packages("pak")
pak::pak("smach/worldcup26")

You need a free football-data.org API key. Register at https://www.football-data.org/client/register and add the key to your ~/.Renviron:

FOOTBALL_DATA_API_KEY=your-key-here

Restart R so the variable is picked up.

Usage

library(worldcup26)

list_teams()
team_schedule("USA")
team_next_match("Brazil")
team_past_results("Argentina")

Team lookups are flexible. All of these resolve to the same team:

team_schedule("United States")
team_schedule("USA")
team_schedule("us")

Common aliases also work: "Korea" → South Korea, "Czech Republic" → Czechia, "Cape Verde" → Cape Verde Islands.

Times default to US Eastern (EDT)

Every World Cup venue is in North America, so dates and times default to US Eastern Time. team_schedule(), team_next_match(), team_past_results(), and all_matches() return a match_date (a Date) and a human-readable kickoff (e.g. "9:00 PM EDT"). all_matches() also keeps the raw utc_date (UTC POSIXct) for ordering.

Pass a tz argument (any name from OlsonNames()) to see another zone — match_date and kickoff are recomputed and the label changes to match:

team_schedule("USA")                              # 9:00 PM EDT
team_schedule("USA", tz = "America/Los_Angeles")  # 6:00 PM PDT
team_schedule("USA", tz = "Europe/London")        # next-day BST

The companion Quarto site has a time-zone picker that does the same thing for visitors, defaulting to their browser's local zone.

Score display

Match scores are reported in a single score_display column:

Situation Value
Match yet to start ""
Match in progress (free tier) "in progress"
Match in progress (live mode) e.g. "1-0 (live)"
Recently completed, no score posted yet "no score available yet"
Completed match with score e.g. "2–1"
Knockout decided on penalties e.g. "1–1 (4–3 PK)"
Postponed / cancelled / suspended "postponed", etc.

By default the package uses football-data.org’s free tier, which doesn’t expose up-to-the-minute scores, so during a match you’ll see "in progress". To get running scorelines instead, enable live mode (see below).

Live scores (optional paid tier)

The free tier only returns delayed scores. If you want live in-match scorelines, football-data.org’s paid tiers use the same API — the cheapest one that includes live scores is the “Free w/ Livescores” tier (€12/month), and it covers the World Cup. No code changes are needed; you just turn live mode on.

Set the WORLDCUP26_LIVE environment variable in your ~/.Renviron (alongside your paid FOOTBALL_DATA_API_KEY):

WORLDCUP26_LIVE=true

Restart R. (For a single session you can instead use options(worldcup26.live = TRUE), which overrides the env var.)

With live mode on:

  • In-progress matches show a running scoreline, e.g. "1-0 (live)", instead of "in progress".
  • The on-disk cache TTL drops to 60 seconds (from 1 hour) so data stays fresh; the chat dashboard and direct function calls are then genuinely live.
  • The chat greeting and the Quarto site’s banner switch to live wording.

For the public Quarto site, also:

  1. Put the paid key in the FOOTBALL_DATA_API_KEY repository secret.
  2. Add a repository variable WORLDCUP26_LIVE set to true (Settings → Secrets and variables → Actions → Variables).

That activates a second cron in .github/workflows/publish.yml that rebuilds the site about every 10 minutes during match windows. Note the static page is only near-live (GitHub’s scheduler runs on ~5-minute granularity and is often delayed); the live R functions and chat app, which hit the API directly, are truly live. Unset the variable to return the site to the free-tier hourly rebuild.

Caching

API responses are cached to disk for one hour by default (60 seconds in live mode). Override with:

options(worldcup26.cache_ttl = 600)  # 10 minutes
clear_cache()                         # nuke the cache manually

Functions

  • list_teams() — every team in the tournament
  • team_schedule(team) — full schedule for a team
  • team_next_match(team) — earliest upcoming match
  • team_past_results(team) — already-played matches
  • all_matches() — every World Cup match in one tibble
  • chat_data() — the flat matches table used by the chat dashboard
  • worldcup26_chat() — launches the natural-language chat dashboard
  • clear_cache() — drop cached responses

Natural-language chat dashboard

The package ships a small Shiny dashboard built on querychat that lets you ask questions in plain English:

worldcup26::worldcup26_chat()

Sample questions:

  • When is Canada’s next game?
  • Show me all matches on June 15.
  • Which teams are in Group D?
  • List the round of 16 matches.
  • Has Brazil played yet?

The dashboard uses Anthropic Claude by default; you’ll need an ANTHROPIC_API_KEY in your environment (~/.Renviron is a good place). Get a key at https://console.anthropic.com. To use a different provider or model, pass your own client:

worldcup26_chat(client = ellmer::chat_openai(model = "gpt-4o"))
# or change the Claude model:
worldcup26_chat(model = "claude-opus-4-7")

Companion Quarto site

This repo also ships a small Quarto + Observable JS site that lets visitors browse the schedule by team or by date. It builds on top of the package — an R chunk in index.qmd calls list_teams() and all_matches() at render time and hands the data to Observable JS via ojs_define(). The rendered page is fully static and needs no API key to view.

Site files (all excluded from the package build via .Rbuildignore):

  • index.qmd, _quarto.yml, _brand.yml, styles.css — page source
  • .github/workflows/publish.yml — rebuilds the page on every push to main and publishes to the gh-pages branch. A cron schedule rebuilds hourly during the match window (15:00–06:00 UTC / 11 AM–2 AM ET), skipping the overnight lull when no games are on Update: I've changed it to every 12 minutes because I'm impatient :)
  • .github/workflows/R-CMD-check.yaml — runs package checks on Ubuntu and Windows for pushes and pull requests

Local preview

quarto preview
# or render once:
quarto render

You need the package installed (R -e 'devtools::install()' from the repo root) and FOOTBALL_DATA_API_KEY in your environment.

GitHub Pages setup

  1. Add a repository secret named FOOTBALL_DATA_API_KEY with your football-data.org API key (Settings → Secrets and variables → Actions).
  2. Push to main or run the workflow manually from the Actions tab. It renders the site and pushes to gh-pages.
  3. In Settings → Pages, choose Branch: gh-pages as the source.

The GitHub Actions workflow rebuilds the site hourly during the match window (15:00–06:00 UTC) and skips the quiet hours (07:00–14:00 UTC / 3–10 AM ET). Adjust the cron expression in .github/workflows/publish.yml if you want a different cadence. A prominent banner on the page tells visitors whether scores are delayed (free tier) or live; see Live scores to switch the site to the paid tier and a ~10-minute rebuild.

Downloadable data

The hourly site build also publishes the tournament data as plain files on the GitHub Pages site, so you can reuse it from any language without an API key (and without hitting the football-data.org rate limit yourself). Base URL:

https://smach.github.io/worldcup26/data/
File What it is
chat_data.json One row per match, denormalised (team names, three-letter codes, stage labels, scores, status, convenience flags). JSON.
chat_data.csv Same table as CSV.
teams.json / teams.csv Participating teams.
worldcup26.rds list(matches, teams, chat_data) for R, with exact types preserved (POSIXct kickoff times, integer NAs, logicals).
metadata.json Generation timestamp, row counts, and a file index.

Examples:

# R — lossless, no parsing needed:
data <- readRDS(url("https://smach.github.io/worldcup26/data/worldcup26.rds"))
data$chat_data

# R — portable:
matches <- jsonlite::fromJSON("https://smach.github.io/worldcup26/data/chat_data.json")
import pandas as pd
matches = pd.read_csv("https://smach.github.io/worldcup26/data/chat_data.csv")

The data refreshes on the same hourly schedule as the site (match window only — 15:00–06:00 UTC). The is_today, is_upcoming, and is_finished flags are computed as of the generated_utc time in metadata.json; recompute them from utc_date / match_date if you need them relative to a different moment. match_date and kickoff are in US Eastern (EDT) — every World Cup venue is in North America — while utc_date is the raw UTC kickoff instant you can convert to any zone yourself; scores are subject to the free tier's delay (see Score display).

The files are produced by data-raw/publish_data.R, run as a step in .github/workflows/publish.yml.

License

MIT.

About

Get info about the 2026 FIFA Men's World Cup! Ask natural language questions about the data on a Shiny dashboard! View all the info in an HTML page generated by Quarto! R package with natural language Shiny dashboard, and a Quarto website

Topics

Resources

License

Unknown, MIT licenses found

Licenses found

Unknown
LICENSE
MIT
LICENSE.md

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors