// traffic.js: the two things the traffic page cannot do as HTML.
//
// Hovering a chart names the point under the pointer, and the globe
// turns. Everything else on the page is drawn by the server and works
// without this file; the globe's numbers are also the country list
// beside it.
(function () {
"use strict";
// Charts. Each plot carries its points' text in data-hover, already
// formatted by the server, so this file never formats a number.
document.querySelectorAll("[data-hover]").forEach(function (plot) {
var data;
try { data = JSON.parse(plot.getAttribute("data-hover")); } catch (e) { return; }
var n = data.at.length;
if (n < 2) return;
var cross = document.createElement("div");
cross.className = "tr-cross";
var tip = document.createElement("div");
tip.className = "tr-tip";
tip.setAttribute("role", "status");
plot.appendChild(cross);
plot.appendChild(tip);
function show(clientX) {
var box = plot.getBoundingClientRect();
var f = Math.min(1, Math.max(0, (clientX - box.left) / box.width));
var i = Math.round(f * (n - 1));
var x = (i / (n - 1)) * box.width;
cross.style.left = x + "px";
cross.hidden = false;
tip.textContent = "";
var h = document.createElement("b");
h.textContent = data.at[i];
tip.appendChild(h);
data.rows.forEach(function (r) {
var line = document.createElement("div");
var k = document.createElement("span");
k.textContent = r.name;
var v = document.createElement("span");
v.textContent = r.v[i];
line.appendChild(k);
line.appendChild(v);
tip.appendChild(line);
});
tip.hidden = false;
var left = x + 12;
if (left + tip.offsetWidth > box.width) left = x - 12 - tip.offsetWidth;
tip.style.left = Math.max(0, left) + "px";
}
function hide() { cross.hidden = true; tip.hidden = true; }
hide();
plot.addEventListener("pointermove", function (e) { show(e.clientX); });
plot.addEventListener("pointerleave", hide);
});
// The globe.
var el = document.getElementById("tr-globe");
if (!el || !window.d3 || !window.topojson) return;
var counts;
try { counts = JSON.parse(el.getAttribute("data-countries")); } catch (e) { return; }
fetch(el.getAttribute("data-world")).then(function (r) { return r.json(); }).then(function (world) {
var css = getComputedStyle(document.documentElement);
var land = css.getPropertyValue("--rule").trim();
var hi = css.getPropertyValue("--accent").trim();
var max = 1;
Object.keys(counts).forEach(function (k) { max = Math.max(max, counts[k].n); });
// A log scale, so one busy country does not leave the rest blank.
function shade(n) {
var f = Math.log(Math.max(1, n)) / Math.log(Math.max(2, max));
return "color-mix(in srgb, " + hi + " " + Math.round(20 + 80 * f) + "%, " + land + ")";
}
var features = topojson.feature(world, world.objects.countries).features;
var ns = "http://www.w3.org/2000/svg";
var svg = document.createElementNS(ns, "svg");
svg.setAttribute("role", "img");
svg.setAttribute("aria-label", el.getAttribute("aria-label") || "");
el.appendChild(svg);
var tip = document.createElement("div");
tip.className = "tr-tip";
tip.hidden = true;
el.appendChild(tip);
var size, proj, path, paths = [];
var sphere = document.createElementNS(ns, "path");
sphere.setAttribute("class", "tr-sphere");
var grat = document.createElementNS(ns, "path");
grat.setAttribute("class", "tr-grat");
svg.appendChild(sphere);
svg.appendChild(grat);
var graticule = d3.geoGraticule10();
features.forEach(function (f) {
var p = document.createElementNS(ns, "path");
var c = counts[String(+f.id)];
p.setAttribute("class", "tr-land");
p.style.fill = c ? shade(c.n) : land;
if (c) {
p.addEventListener("pointermove", function (e) {
var box = el.getBoundingClientRect();
tip.textContent = "";
var b = document.createElement("b");
b.textContent = c.name;
var d = document.createElement("div");
d.textContent = c.label;
tip.appendChild(b);
tip.appendChild(d);
tip.hidden = false;
tip.style.left = Math.min(e.clientX - box.left + 12, box.width - tip.offsetWidth) + "px";
tip.style.top = (e.clientY - box.top + 12) + "px";
});
p.addEventListener("pointerleave", function () { tip.hidden = true; });
p.addEventListener("click", function () { if (!moved) location.href = c.href; });
p.style.cursor = "pointer";
}
svg.appendChild(p);
paths.push([p, f]);
});
function layout() {
size = el.clientWidth;
svg.setAttribute("viewBox", "0 0 " + size + " " + size);
var rot = proj ? proj.rotate() : [40, -30];
proj = d3.geoOrthographic().scale(size / 2 - 2).translate([size / 2, size / 2]).rotate(rot).clipAngle(90);
path = d3.geoPath(proj);
draw();
}
function draw() {
sphere.setAttribute("d", path({ type: "Sphere" }));
grat.setAttribute("d", path(graticule) || "");
paths.forEach(function (pf) { pf[0].setAttribute("d", path(pf[1]) || ""); });
}
layout();
var spin = !matchMedia("(prefers-reduced-motion: reduce)").matches;
var dragging = false, moved = false, last;
function tick() {
if (spin && !dragging && document.visibilityState === "visible") {
var r = proj.rotate();
proj.rotate([r[0] + 0.12, r[1]]);
draw();
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
svg.addEventListener("pointerdown", function (e) {
dragging = true; moved = false; spin = false; last = [e.clientX, e.clientY];
svg.setPointerCapture(e.pointerId);
});
svg.addEventListener("pointermove", function (e) {
if (!dragging) return;
var k = 75 / proj.scale(), r = proj.rotate();
var dx = e.clientX - last[0], dy = e.clientY - last[1];
if (Math.abs(dx) + Math.abs(dy) > 2) moved = true;
proj.rotate([r[0] + dx * k, Math.max(-80, Math.min(80, r[1] - dy * k))]);
last = [e.clientX, e.clientY];
draw();
});
svg.addEventListener("pointerup", function () { dragging = false; });
var w = el.clientWidth;
addEventListener("resize", function () { if (el.clientWidth !== w) { w = el.clientWidth; layout(); } });
}).catch(function () { /* the list beside it still says everything */ });
})();