Skip to content

Instantly share code, notes, and snippets.

@walkerke
Last active May 23, 2026 23:07
Show Gist options
  • Select an option

  • Save walkerke/c4ae228039259fa2759a4d43e7039372 to your computer and use it in GitHub Desktop.

Select an option

Save walkerke/c4ae228039259fa2759a4d43e7039372 to your computer and use it in GitHub Desktop.
# Day 27: Boundaries - World Time Zones
# The invisible lines that govern our daily lives
# Uses current IANA time zone rules, including daylight saving time at render time.
library(mapgl)
library(sf)
library(dplyr)
library(lubridate)
tz_release <- "2026b"
# "now" is lighter and groups places by current/future clock behavior.
# Use "1970" if you want the finer historical IANA zones, like Indiana's legacy intricacies.
tz_scope <- "now"
# Xinjiang has overlapping official China time (UTC+8) and local Urumqi time (UTC+6) usage.
# Use "dual" to show both in the popup, "official" for Beijing/China Standard Time,
# "local" for Urumqi/Xinjiang Time, or "both" to preserve the source overlap.
xinjiang_policy <- "dual"
tz_url <- paste0(
"https://github.com/evansiroky/timezone-boundary-builder/releases/download/",
tz_release,
"/timezones-with-oceans-",
tz_scope,
".geojson.zip"
)
utc_now <- with_tz(Sys.time(), "UTC")
user_tz <- Sys.timezone()
user_time <- with_tz(utc_now, user_tz)
read_timezone_boundaries <- function(url) {
temp_zip <- tempfile(fileext = ".zip")
temp_dir <- tempfile("timezone-boundaries-")
dir.create(temp_dir)
on.exit(unlink(c(temp_zip, temp_dir), recursive = TRUE), add = TRUE)
download.file(url, temp_zip, mode = "wb", quiet = TRUE)
unzip(temp_zip, exdir = temp_dir)
geojson <- list.files(temp_dir, pattern = "\\.json$", full.names = TRUE)
if (length(geojson) != 1) {
stop("Expected exactly one GeoJSON file in timezone boundary archive.")
}
st_read(geojson, quiet = TRUE)
}
offset_minutes <- function(time, tz) {
if (is.na(tz) || !tz %in% OlsonNames()) {
return(NA_integer_)
}
offset <- format(time, tz = tz, format = "%z")
sign <- ifelse(substr(offset, 1, 1) == "-", -1L, 1L)
hours <- as.integer(substr(offset, 2, 3))
minutes <- as.integer(substr(offset, 4, 5))
sign * (hours * 60L + minutes)
}
offset_label <- function(minutes) {
if (is.na(minutes)) {
return("UTC unknown")
}
if (minutes == 0) {
return("UTC")
}
sign <- ifelse(minutes > 0, "+", "-")
abs_minutes <- abs(minutes)
hours <- abs_minutes %/% 60L
mins <- abs_minutes %% 60L
if (mins == 0) {
paste0("UTC", sign, hours)
} else {
paste0("UTC", sign, hours, ":", sprintf("%02d", mins))
}
}
format_time_in_tz <- function(time, tz, format) {
if (is.na(tz) || !tz %in% OlsonNames()) {
return(NA_character_)
}
format(time, tz = tz, format = format)
}
resolve_xinjiang_overlap <- function(zones, policy = c("dual", "official", "local", "both")) {
policy <- match.arg(policy)
zones$alternate_tzid <- NA_character_
zones$zone_label_override <- NA_character_
zones$zone_note <- NA_character_
if (policy == "both") {
return(zones)
}
# In the "now" file, Asia/Manila represents current UTC+8 behavior, including China.
# Asia/Dhaka represents current UTC+6 behavior, including local Xinjiang/Urumqi time.
official_tz <- "Asia/Manila"
local_tz <- "Asia/Dhaka"
if (!all(c(official_tz, local_tz) %in% zones$tzid)) {
return(zones)
}
old_s2 <- sf_use_s2()
on.exit(suppressMessages(sf_use_s2(old_s2)), add = TRUE)
suppressMessages(sf_use_s2(FALSE))
zones <- st_make_valid(zones)
official_idx <- which(zones$tzid == official_tz)
local_idx <- which(zones$tzid == local_tz)
zones_work <- st_make_valid(st_transform(zones, 6933))
overlap <- suppressMessages(suppressWarnings(st_intersection(
st_geometry(zones_work[official_idx, ]),
st_geometry(zones_work[local_idx, ])
)))
if (length(overlap) == 0) {
return(zones)
}
overlap <- suppressMessages(suppressWarnings(
st_collection_extract(st_make_valid(st_union(overlap)), "POLYGON")
))
xinjiang_extent <- st_as_sfc(st_bbox(
c(xmin = 73, ymin = 34, xmax = 97, ymax = 50),
crs = st_crs(zones)
))
xinjiang_extent <- st_transform(xinjiang_extent, st_crs(zones_work))
overlap <- suppressMessages(suppressWarnings(st_intersection(overlap, xinjiang_extent)))
trim_zone <- function(zone_idx) {
trimmed <- suppressMessages(suppressWarnings(st_difference(st_geometry(zones_work[zone_idx, ]), overlap)))
trimmed <- suppressMessages(suppressWarnings(st_collection_extract(st_make_valid(trimmed), "POLYGON")))
trimmed <- suppressMessages(suppressWarnings(st_transform(st_union(trimmed), st_crs(zones))))
st_geometry(zones)[zone_idx] <<- trimmed
}
if (policy == "official") {
trim_zone(local_idx)
return(zones)
}
if (policy == "local") {
trim_zone(official_idx)
return(zones)
}
xinjiang <- zones[official_idx, ]
st_geometry(xinjiang) <- suppressMessages(suppressWarnings(st_transform(st_union(overlap), st_crs(zones))))
xinjiang$alternate_tzid <- local_tz
xinjiang$zone_label_override <- "Xinjiang / Urumqi"
xinjiang$zone_note <- "Official China time and local Urumqi time are both shown."
zones <- rbind(zones, xinjiang)
zones
}
timezones <- read_timezone_boundaries(tz_url)
timezones <- resolve_xinjiang_overlap(timezones, xinjiang_policy)
valid_tz <- timezones$tzid %in% OlsonNames()
if (any(!valid_tz)) {
warning(
"Some boundary time zone IDs are not available in this R/system tz database: ",
paste(timezones$tzid[!valid_tz], collapse = ", ")
)
}
timezones <- timezones |>
mutate(
offset_minutes = vapply(tzid, function(tz) offset_minutes(utc_now, tz), integer(1)),
utc_offset = offset_minutes / 60,
current_time = vapply(tzid, function(tz) format_time_in_tz(utc_now, tz, "%H:%M"), character(1)),
time_ampm = vapply(tzid, function(tz) format_time_in_tz(utc_now, tz, "%I:%M %p"), character(1)),
date_display = vapply(tzid, function(tz) format_time_in_tz(utc_now, tz, "%b %d"), character(1)),
zone_abbr = vapply(tzid, function(tz) format_time_in_tz(utc_now, tz, "%Z"), character(1)),
offset_label = vapply(offset_minutes, offset_label, character(1)),
tooltip_title = if_else(is.na(current_time), "Time unavailable", current_time),
alternate_time = vapply(alternate_tzid, function(tz) format_time_in_tz(utc_now, tz, "%H:%M"), character(1)),
alternate_ampm = vapply(alternate_tzid, function(tz) format_time_in_tz(utc_now, tz, "%I:%M %p"), character(1)),
alternate_abbr = vapply(alternate_tzid, function(tz) format_time_in_tz(utc_now, tz, "%Z"), character(1)),
alternate_offset = vapply(
vapply(alternate_tzid, function(tz) offset_minutes(utc_now, tz), integer(1)),
offset_label,
character(1)
),
primary_time_label = if_else(is.na(alternate_tzid), "Local time", "Official China time"),
alternate_label = if_else(
is.na(alternate_tzid),
"",
paste0(
"<div style='margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.14);'>",
"<div style='font-size: 11px; color: #999; text-transform: uppercase; letter-spacing: 0.4px;'>Local Urumqi Time</div>",
"<div style='font-size: 20px; font-weight: 700; color: #fff;'>",
alternate_time,
"</div>",
"<div style='font-size: 12px; color: #888;'>",
alternate_ampm,
" ",
alternate_abbr,
" / ",
alternate_offset,
"</div>",
"</div>"
)
),
note_label = if_else(
is.na(zone_note),
"",
paste0("<div style='font-size: 11px; color: #777; margin-top: 8px; max-width: 220px;'>", zone_note, "</div>")
),
zone_label = coalesce(zone_label_override, gsub("_", " ", tzid)),
is_user_tz = tzid == user_tz
)
xinjiang_timezones <- timezones |>
filter(!is.na(alternate_tzid))
timezones <- timezones |>
filter(is.na(alternate_tzid))
user_offset_minutes <- offset_minutes(utc_now, user_tz)
user_offset_label <- offset_label(user_offset_minutes)
user_zone <- timezones |>
filter(is_user_tz)
if (nrow(user_zone) > 0) {
user_bbox <- st_bbox(user_zone)
user_center_lon <- mean(c(user_bbox[["xmin"]], user_bbox[["xmax"]]))
user_center_lat <- mean(c(user_bbox[["ymin"]], user_bbox[["ymax"]]))
user_center_lat <- max(min(user_center_lat, 55), -55)
} else {
user_center_lon <- (user_offset_minutes / 60) * 15
user_center_lat <- 20
}
cat("Your timezone:", user_tz, "\n")
cat("Your current time:", format(user_time, "%Y-%m-%d %H:%M:%S %Z"), "\n")
cat("UTC offset:", user_offset_label, "\n")
cat("Timezone boundary release:", tz_release, "\n")
cat("Timezone boundary scope:", tz_scope, "\n")
cat("Xinjiang policy:", xinjiang_policy, "\n")
# Time zone color palette - dawn to dusk gradient
tz_palette <- c(
"#1e3a5f", # UTC-12
"#2d4a6f", # UTC-10
"#3d5a7f", # UTC-8
"#5a7a9f", # UTC-6
"#7a9abf", # UTC-4
"#9abadf", # UTC-2
"#f0f4f8", # UTC
"#ffe4b5", # UTC+2
"#ffc987", # UTC+4
"#ffaa5c", # UTC+6
"#ff8533", # UTC+8
"#ff5500", # UTC+10
"#cc3300", # UTC+12
"#8f2200" # UTC+14
)
generated_label <- format(user_time, "%b %d, %Y %H:%M %Z")
info_html <- paste0(
"<div style='font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; ",
"background: rgba(20, 20, 30, 0.95); padding: 16px 20px; border-radius: 8px; ",
"border: 1px solid rgba(255, 255, 255, 0.1); max-width: 330px;'>",
"<div style='font-size: 18px; font-weight: 700; color: #fff; margin-bottom: 10px;'>",
"World Time Zones</div>",
"<div style='background: rgba(255, 200, 100, 0.15); border: 1px solid rgba(255, 200, 100, 0.4); ",
"border-radius: 6px; padding: 10px 12px; margin-bottom: 12px;'>",
"<div style='font-size: 11px; color: #ffc864; text-transform: uppercase; letter-spacing: 0.5px;'>Your Time</div>",
"<div style='font-size: 24px; font-weight: 700; color: #fff;'>",
format(user_time, "%H:%M"),
"</div>",
"<div style='font-size: 12px; color: #aaa;'>",
user_tz,
" (",
user_offset_label,
")",
"</div>",
"</div>",
"<div style='font-size: 12px; color: #888; line-height: 1.5;'>",
"Hover over any zone to see the local time calculated from IANA time zone rules, including daylight saving where applicable.</div>",
"<div style='font-size: 10px; color: #555; margin-top: 10px; line-height: 1.4;'>",
"Calculated at ",
generated_label,
". Current-equivalent IANA zones from timezone-boundary-builder ",
tz_release,
" / OpenStreetMap. Xinjiang: ",
case_when(
xinjiang_policy == "official" ~ "official China time",
xinjiang_policy == "local" ~ "local Urumqi time",
xinjiang_policy == "dual" ~ "both official China time and local Urumqi time shown",
TRUE ~ "source overlap preserved"
),
".</div>",
"</div>"
)
maplibre(
style = carto_style("dark-matter"),
center = c(user_center_lon, user_center_lat),
zoom = 2
) |>
add_fill_layer(
id = "timezone-fill",
source = timezones,
fill_color = interpolate(
column = "utc_offset",
values = c(-12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14),
stops = tz_palette,
na_color = "#333333"
),
fill_opacity = 0.72,
hover_options = list(
fill_opacity = 0.95
),
tooltip = concat(
"<div style='background: #1a1a2e; padding: 14px 18px; border-radius: 8px; ",
"border: 1px solid rgba(255, 255, 255, 0.2); font-family: system-ui;'>",
"<div style='font-size: 32px; font-weight: 700; color: #fff;'>",
get_column("tooltip_title"),
"</div>",
"<div style='font-size: 13px; color: #888; margin-bottom: 8px;'>",
get_column("primary_time_label"),
": ",
get_column("time_ampm"),
" ",
get_column("zone_abbr"),
"</div>",
"<div style='font-size: 13px; color: #aaa; margin-bottom: 8px;'>",
get_column("date_display"),
"</div>",
"<div style='font-size: 14px; font-weight: 600; color: #ffc864;'>",
get_column("offset_label"),
"</div>",
get_column("alternate_label"),
"<div style='font-size: 11px; color: #666; margin-top: 6px;'>",
get_column("zone_label"),
"</div>",
get_column("note_label"),
"</div>"
)
) |>
add_fill_layer(
id = "timezone-xinjiang-dual",
source = xinjiang_timezones,
fill_color = interpolate(
column = "utc_offset",
values = c(-12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14),
stops = tz_palette,
na_color = "#333333"
),
fill_opacity = 0.82,
hover_options = list(
fill_opacity = 0.98
),
tooltip = concat(
"<div style='background: #1a1a2e; padding: 14px 18px; border-radius: 8px; ",
"border: 1px solid rgba(255, 255, 255, 0.2); font-family: system-ui;'>",
"<div style='font-size: 32px; font-weight: 700; color: #fff;'>",
get_column("tooltip_title"),
"</div>",
"<div style='font-size: 13px; color: #888; margin-bottom: 8px;'>",
get_column("primary_time_label"),
": ",
get_column("time_ampm"),
" ",
get_column("zone_abbr"),
"</div>",
"<div style='font-size: 13px; color: #aaa; margin-bottom: 8px;'>",
get_column("date_display"),
"</div>",
"<div style='font-size: 14px; font-weight: 600; color: #ffc864;'>",
get_column("offset_label"),
"</div>",
get_column("alternate_label"),
"<div style='font-size: 11px; color: #666; margin-top: 6px;'>",
get_column("zone_label"),
"</div>",
get_column("note_label"),
"</div>"
)
) |>
add_line_layer(
id = "timezone-lines",
source = timezones,
line_color = "#ffffff",
line_width = interpolate(
property = "zoom",
values = c(1, 4, 8),
stops = c(0.25, 0.8, 1.5)
),
line_opacity = 0.35
) |>
add_control(
html = info_html,
position = "top-left"
) |>
add_legend(
legend_title = "Current UTC Offset",
values = c("-12", "-6", "0", "+6", "+14"),
colors = c("#1e3a5f", "#5a7a9f", "#f0f4f8", "#ffaa5c", "#8f2200"),
type = "continuous",
position = "bottom-left"
) |>
add_navigation_control(position = "top-right") |>
add_fullscreen_control(position = "top-right")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment