// World map web component for brewlog stats page. // Map data: Al MacDonald, edited by Fritz Lekschas. License: CC BY-SA 3.0 const WORLD_SVG = ` Author: Al MacDonald Editor: Fritz Lekschas License: CC BY-SA 3.0 ID: ISO 3166-1 or "_[a-zA-Z]" if an ISO code is not available`; customElements.define( "world-map", class extends HTMLElement { static get observedAttributes() { return ["data-countries", "data-max", "data-selected"]; } connectedCallback() { this._scheduleRender(); this._themeObserver = new MutationObserver(() => requestAnimationFrame(() => this._recolor()), ); this._themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"], }); } disconnectedCallback() { this._themeObserver?.disconnect(); this._themeObserver = null; } attributeChangedCallback(name) { if (!this.isConnected) return; if (name === "data-selected") { this._recolor(); } else { this._scheduleRender(); } } _scheduleRender() { if (this._pendingRender) return; this._pendingRender = true; requestAnimationFrame(() => { this._pendingRender = false; this._render(); }); } _parseCountries() { const attr = this.getAttribute("data-countries") || ""; const counts = new Map(); if (attr) { attr.split(",").forEach((pair) => { const [code, count] = pair.split(":"); if (code && count) counts.set(code.trim().toLowerCase(), parseInt(count, 10)); }); } this._counts = counts; this._max = parseInt(this.getAttribute("data-max") || "1", 10) || 1; } _render() { this._parseCountries(); this.innerHTML = ""; const container = document.createElement("div"); container.innerHTML = WORLD_SVG; const svg = container.querySelector("svg"); if (!svg) return; svg.style.width = "100%"; svg.style.height = "auto"; svg.style.display = "block"; this.appendChild(svg); svg.setAttribute("role", "img"); svg.setAttribute( "aria-label", "World map showing coffee origins by country", ); const srOnly = this.querySelector(".sr-only"); if (srOnly) srOnly.remove(); const raw = this.dataset.countries || ""; if (raw) { const entries = raw.split(",").map((e) => e.split(":")); const sr = document.createElement("div"); sr.className = "sr-only"; sr.innerHTML = "" + entries .map( ([code, count]) => ``, ) .join("") + "
Coffee origins by country
CountryCount
${code.toUpperCase()}${count}
"; this.appendChild(sr); } this._recolor(); } _recolor() { const svg = this.querySelector("svg"); if (!svg) return; if (!this._counts) this._parseCountries(); const selected = (this.getAttribute("data-selected") || "").toLowerCase(); const counts = this._counts; const max = this._max; const styles = getComputedStyle(document.documentElement); const rgb = styles.getPropertyValue("--highlight-rgb").trim() || "185, 28, 28"; const borderColor = styles.getPropertyValue("--text-muted").trim() || "#9ca3af"; const surfaceAlt = styles.getPropertyValue("--surface-alt").trim() || "#f5f5f4"; const borderMuted = styles.getPropertyValue("--border").trim() || "#d6d3d1"; const applyStyle = (el, fill) => { el.style.fill = fill; el.style.stroke = borderColor; el.style.strokeWidth = "0.3"; }; const colorFor = (code) => { const count = counts.get(code); if (selected) { if (code === selected && count) return `rgba(${rgb}, 1)`; return count ? borderMuted : surfaceAlt; } if (count) { const alpha = (0.15 + 0.85 * (count / max)).toFixed(2); return `rgba(${rgb}, ${alpha})`; } return surfaceAlt; }; svg.querySelectorAll("path[id], g[id]").forEach((el) => { const code = el.id.toLowerCase(); const fill = colorFor(code); if (el.tagName === "g") { el.querySelectorAll("path").forEach((p) => applyStyle(p, fill)); } else { applyStyle(el, fill); } }); } }, );