Skip to content
quarry Artifact [168ddbafc4]
Login

Artifact [168ddbafc4]

Artifact 168ddbafc48fbbb85b0780d7b16daddc378435f5def2f2e35d78b24076e7b405:


// 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 */ });
})();