PageSourceSearch

https://www.ziptrak.com.au/wp-content/themes/wp-theme-ziptrak/includes/js/components/globe-widget.js

js ziptrak.com.au collected 2026-10-08 01:07:33 UTC 44,467 bytes, 1,481 lines download raw bytes

1(() => {
2	'use strict';
3
4	const DOT_OPACITY = 0.2;
5	const DEFAULT_FOCUS_LON_OFFSET = 34;
6	const DEFAULT_FOCUS_LAT_OFFSET = 34;
7	const ROTATION_DURATION = 700;
8	const ROTATION_MIN_FRAME_INTERVAL = 1000 / 30;
9	const AUTO_SCROLL_SUPPRESS_DURATION = 1400;
10	const SECTION_SCROLL_TIMEOUT = 1800;
11	const TOOLTIP_EDGE_OFFSET = 8;
12	const TOOLTIP_BOTTOM_OFFSET = 16;
13	const TOOLTIP_RIGHT_OFFSET = 24;
14	const FOCUSED_AVAILABLE_COLOR = '#BFA94F';
15	const COUNTRY_DESCRIPTION = 'Default country description.';
16
17	const globalConfig = window.ZiptrakGlobeConfig || {};
18	const defaults = globalConfig.defaults || {};
19	const worldDataUrl = globalConfig.worldDataUrl || '';
20	const worldAdmin1DataUrl = globalConfig.worldAdmin1DataUrl || '';
21	const countryMetaUrl = globalConfig.countryMetaUrl || '';
22	const fallbackCountriesUrl = globalConfig.fallbackCountriesUrl || '';
23	let cachedDataPromise = null;
24
25	const normalizeToken = (value) => String(value || '')
26		.toUpperCase()
27		.replace(/[^A-Z0-9]/g, '');
28
29	const normalizeStateToken = (value) => {
30		const input = String(value || '');
31		const ascii = typeof input.normalize === 'function'
32			? input.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
33			: input;
34		return normalizeToken(ascii);
35	};
36
37	const normalizeIsoN3 = (value) => {
38		const raw = String(value || '').replace(/[^0-9]/g, '');
39		if (!raw) {
40			return '';
41		}
42
43		return `000${raw}`.slice(-3);
44	};
45
46	const parseCountryTokens = (value) => {
47		if (Array.isArray(value)) {
48			return value
49				.map((item) => String(item || '').trim())
50				.filter((item) => item !== '');
51		}
52
53		if (typeof value === 'string') {
54			return value
55				.split(',')
56				.map((item) => item.trim())
57				.filter((item) => item !== '');
58		}
59
60		return [];
61	};
62
63	const buildFallbackCountryMap = (items) => {
64		const map = {};
65
66		(items || []).forEach((item) => {
67			const iso2 = String(item.iso2 || item.iso_a2 || '').toUpperCase();
68			const iso3 = String(item.iso3 || item.iso_a3 || '').toUpperCase();
69			const id = normalizeIsoN3(item.isoN3 || item.iso_n3 || item.id || '');
70			const name = String(item.name_long || item.name || iso2 || iso3 || id || 'Unknown');
71			const centroid = Array.isArray(item.centroid) && item.centroid.length === 2
72				? [Number(item.centroid[0]), Number(item.centroid[1])]
73				: null;
74
75			if (!centroid || !isFinite(centroid[0]) || !isFinite(centroid[1])) {
76				return;
77			}
78
79			const info = {
80				id,
81				iso2,
82				iso3,
83				name,
84				centroid,
85				nameNorm: normalizeToken(name),
86				idNorm: normalizeToken(id),
87				iso2Norm: normalizeToken(iso2),
88				iso3Norm: normalizeToken(iso3),
89				isVirtual: true
90			};
91
92			[info.nameNorm, info.idNorm, info.iso2Norm, info.iso3Norm].forEach((token) => {
93				if (token) {
94					map[token] = info;
95				}
96			});
97		});
98
99		return map;
100	};
101
102	const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
103	const escapeHtml = (value) => String(value || '')
104		.replace(/&/g, '&')
105		.replace(/</g, '&lt;')
106		.replace(/>/g, '&gt;')
107		.replace(/"/g, '&quot;')
108		.replace(/'/g, '&#39;');
109
110	const mergeConfig = (config) => {
111		const clickableCountriesValue = config.clickableCountries != null
112			? config.clickableCountries
113			: defaults.clickableCountries;
114
115		return {
116			height: Number(config.height || 520),
117			rotationSpeed: Number(config.rotationSpeed != null ? config.rotationSpeed : defaults.rotationSpeed || 0.2),
118			globeOpacity: clamp(Number(config.globeOpacity != null ? config.globeOpacity : defaults.globeOpacity != null ? defaults.globeOpacity : 1), 0, 1),
119			backsideDotOpacity: clamp(Number(config.backsideDotOpacity != null ? config.backsideDotOpacity : defaults.backsideDotOpacity != null ? defaults.backsideDotOpacity : DOT_OPACITY), 0, 1),
120			focusLonOffset: clamp(Number(config.focusLonOffset != null ? config.focusLonOffset : defaults.focusLonOffset != null ? defaults.focusLonOffset : DEFAULT_FOCUS_LON_OFFSET), -89, 89),
121			focusLatOffset: clamp(Number(config.focusLatOffset != null ? config.focusLatOffset : defaults.focusLatOffset != null ? defaults.focusLatOffset : DEFAULT_FOCUS_LAT_OFFSET), -89, 89),
122			oceanColor: config.oceanColor || defaults.oceanColor || '#0B2F54',
123			landColor: config.landColor || defaults.landColor || '#6D9F74',
124			borderColor: config.borderColor || defaults.borderColor || '#9BB8D1',
125			clickableColor: config.clickableColor || defaults.clickableColor || '#FFC03C',
126			clickableHoverColor: config.clickableHoverColor || defaults.clickableHoverColor || '#FFE27A',
127			focusedAvailableColor: config.focusedAvailableColor || defaults.focusedAvailableColor || FOCUSED_AVAILABLE_COLOR,
128			coreCountry: String(config.coreCountry != null ? config.coreCountry : defaults.coreCountry || '').trim(),
129			clickableCountries: parseCountryTokens(clickableCountriesValue),
130			countryLinks: config.countryLinks && typeof config.countryLinks === 'object' ? config.countryLinks : {},
131			countryProfiles: config.countryProfiles && typeof config.countryProfiles === 'object'
132				? config.countryProfiles
133				: (defaults.countryProfiles && typeof defaults.countryProfiles === 'object' ? defaults.countryProfiles : {})
134		};
135	};
136
137	const buildLinkMap = (links) => {
138		const map = {};
139		Object.keys(links || {}).forEach((key) => {
140			const normalized = normalizeToken(key);
141			const href = String(links[key] || '').trim();
142			if (normalized && href) {
143				map[normalized] = href;
144			}
145		});
146
147		return map;
148	};
149
150	const getWorldData = () => {
151		if (cachedDataPromise) {
152			return cachedDataPromise;
153		}
154
155		const fallbackCountriesPromise = fallbackCountriesUrl
156			? d3.json(fallbackCountriesUrl).catch(() => [])
157			: Promise.resolve([]);
158
159		cachedDataPromise = Promise.all([
160			d3.json(worldDataUrl),
161			d3.json(worldAdmin1DataUrl).catch(() => null),
162			d3.tsv(countryMetaUrl).catch(() => []),
163			fallbackCountriesPromise
164		]).then(([worldData, admin1Data, rows, fallbackPayload]) => {
165			const countryMetaById = {};
166			(rows || []).forEach((row) => {
167				const key = normalizeIsoN3(row.iso_n3);
168				if (key) {
169					countryMetaById[key] = row;
170				}
171			});
172
173			const admin1Features = admin1Data && Array.isArray(admin1Data.features)
174				? admin1Data.features
175				: [];
176			const fallbackRows = Array.isArray(fallbackPayload)
177				? fallbackPayload
178				: (fallbackPayload && Array.isArray(fallbackPayload.countries) ? fallbackPayload.countries : []);
179			const fallbackCountryByToken = buildFallbackCountryMap(fallbackRows);
180			const primaryPolygonOnlyRows = fallbackPayload && Array.isArray(fallbackPayload.primaryPolygonOnlyCountries)
181				? fallbackPayload.primaryPolygonOnlyCountries
182				: [];
183			const primaryPolygonOnlyTokenSet = {};
184			primaryPolygonOnlyRows.forEach((value) => {
185				const token = normalizeToken(value);
186				if (token) {
187					primaryPolygonOnlyTokenSet[token] = true;
188				}
189			});
190
191			return {
192				worldData,
193				admin1Features,
194				countryMetaById,
195				fallbackCountryByToken,
196				primaryPolygonOnlyTokenSet
197			};
198		});
199
200		return cachedDataPromise;
201	};
202
203	const getCountryInfo = (feature, countryMetaById) => {
204		const id = normalizeIsoN3(feature && feature.id);
205		const meta = countryMetaById[id] || {};
206		const name = meta.name_long || meta.name || id;
207		const iso2 = String(meta.iso_a2 || '').toUpperCase();
208		const iso3 = String(meta.iso_a3 || '').toUpperCase();
209
210		return {
211			id,
212			iso2,
213			iso3,
214			name,
215			nameNorm: normalizeToken(name),
216			idNorm: normalizeToken(id),
217			iso2Norm: normalizeToken(iso2),
218			iso3Norm: normalizeToken(iso3)
219		};
220	};
221
222	const createPulse = (selection) => {
223		selection
224			.attr('r', 3)
225			.attr('fill', '#fff')
226			.attr('fill-opacity', 0.95)
227			.attr('stroke', '#fff')
228			.attr('stroke-width', 1)
229			.attr('stroke-opacity', 0.8)
230			.transition()
231			.duration(1400)
232			.ease(d3.easeCubicOut)
233			.attr('r', 9)
234			.attr('stroke-opacity', 0)
235			.attr('fill-opacity', 0)
236			.on('end', function pulseEnd() {
237				createPulse(d3.select(this));
238			});
239	};
240
241	const isFrontHemisphere = (projection, lonLat) => {
242		const rotate = projection.rotate();
243		const center = [-rotate[0], -rotate[1]];
244		return d3.geoDistance(center, lonLat) <= (Math.PI / 2);
245	};
246
247	const getFocusRotateForCentroid = (centroid, lonOffset, latOffset) => {
248		const lon = centroid[0];
249		const lat = centroid[1];
250		return [
251			-lon + lonOffset,
252			clamp(-(lat + latOffset), -89, 89),
253			0
254		];
255	};
256
257	const setupWidget = (
258		widget,
259		worldData,
260		admin1Features,
261		countryMetaById,
262		fallbackCountryByToken = {},
263		primaryPolygonOnlyTokenSet = {}
264	) => {
265		if (typeof window.d3 === 'undefined' || typeof window.topojson === 'undefined') {
266			return;
267		}
268
269		const isMobileOrTabletViewport = () => window.matchMedia('(max-width: 991px)').matches;
270		const rawConfig = JSON.parse(widget.getAttribute('data-globe-config') || '{}') || {};
271		const config = mergeConfig(rawConfig);
272		const backsideDotOpacity = config.backsideDotOpacity;
273		const configuredHeight = config.height > 0 ? config.height : 520;
274
275		const getViewportSize = () => {
276			const parentRect = widget.parentElement
277				? widget.parentElement.getBoundingClientRect()
278				: null;
279			const parentHeight = parentRect ? Math.round(parentRect.height) : 0;
280			const parentWidth = parentRect ? Math.round(parentRect.width) : 0;
281			const mobileOrTablet = isMobileOrTabletViewport();
282			const viewportWidth = Math.round(window.innerWidth || document.documentElement.clientWidth || 0);
283			let nextWidth = mobileOrTablet
284				? Math.max(280, parentWidth || widget.clientWidth || 800)
285				: Math.max(280, widget.clientWidth || 800);
286			let nextHeight = (mobileOrTablet && parentHeight > 0)
287				? parentHeight
288				: configuredHeight;
289
290			if (mobileOrTablet) {
291				nextWidth = Math.max(280, viewportWidth || nextWidth);
292				nextHeight = nextWidth;
293			}
294
295			return { width: nextWidth, height: nextHeight };
296		};
297
298		let width = 0;
299		let height = 0;
300		const applyWidgetSize = () => {
301			const size = getViewportSize();
302			width = size.width;
303			height = size.height;
304			widget.style.height = `${height}px`;
305			return size;
306		};
307
308		applyWidgetSize();
309		widget.style.opacity = `${config.globeOpacity}`;
310
311		const clickableSet = {};
312		config.clickableCountries.forEach((item) => {
313			const token = normalizeToken(item);
314			if (token) {
315				clickableSet[token] = true;
316			}
317		});
318
319		const coreCountryToken = normalizeToken(config.coreCountry);
320		const linksByKey = buildLinkMap(config.countryLinks);
321		const profileByKey = {};
322		Object.keys(config.countryProfiles || {}).forEach((isoCode) => {
323			const key = normalizeToken(isoCode);
324			if (key) {
325				profileByKey[key] = config.countryProfiles[isoCode];
326			}
327		});
328		const countries = topojson.feature(worldData, worldData.objects.countries).features;
329		const borders = topojson.mesh(worldData, worldData.objects.countries, (a, b) => a !== b);
330
331		const getCountryProfile = (info) => (
332			profileByKey[info.iso2Norm] ||
333			profileByKey[info.iso3Norm] ||
334			profileByKey[info.idNorm] ||
335			profileByKey[info.nameNorm] ||
336			null
337		);
338
339		const getStateTokenFromProfile = (info) => {
340			const profile = getCountryProfile(info);
341			if (!profile || !profile.state) {
342				return '';
343			}
344			return normalizeStateToken(profile.state);
345		};
346
347		const getAdmin1CountryTokens = (properties) => {
348			const tokens = {};
349			[
350				properties && properties.iso_a2,
351				properties && properties.adm0_a3,
352				properties && properties.adm0_name,
353				properties && properties.geonunit,
354				properties && properties.admin
355			].forEach((value) => {
356				const token = normalizeToken(value);
357				if (token) {
358					tokens[token] = true;
359				}
360			});
361			return tokens;
362		};
363
364		const getAdmin1StateTokens = (properties) => {
365			const tokens = {};
366			[
367				properties && properties.name,
368				properties && properties.name_en,
369				properties && properties.name_alt,
370				properties && properties.postal,
371				properties && properties.iso_3166_2,
372				properties && properties.code_hasc
373			].forEach((value) => {
374				const stringValue = String(value || '').trim();
375				if (!stringValue) {
376					return;
377				}
378
379				stringValue.split('|').forEach((part) => {
380					const normalized = normalizeStateToken(part);
381					if (normalized) {
382						tokens[normalized] = true;
383					}
384
385					const dashIndex = part.indexOf('-');
386					if (dashIndex > 0) {
387						const suffix = normalizeStateToken(part.slice(dashIndex + 1));
388						if (suffix) {
389							tokens[suffix] = true;
390						}
391					}
392
393					const dotIndex = part.indexOf('.');
394					if (dotIndex > 0) {
395						const suffix = normalizeStateToken(part.slice(dotIndex + 1));
396						if (suffix) {
397							tokens[suffix] = true;
398						}
399					}
400				});
401			});
402			return tokens;
403		};
404
405		const getCountryLink = (info) => {
406			const profile = getCountryProfile(info);
407			return (
408				(profile && profile.pageLink) ||
409				linksByKey[info.nameNorm] ||
410				linksByKey[info.iso2Norm] ||
411				linksByKey[info.iso3Norm] ||
412				linksByKey[info.idNorm] ||
413				''
414			);
415		};
416
417		const isCoreCountry = (info) => Boolean(
418			coreCountryToken &&
419			(
420				coreCountryToken === info.nameNorm ||
421				coreCountryToken === info.iso2Norm ||
422				coreCountryToken === info.iso3Norm ||
423				coreCountryToken === info.idNorm
424			)
425		);
426
427		const isClickableCountry = (info) => Boolean(
428			clickableSet[info.nameNorm] ||
429			clickableSet[info.iso2Norm] ||
430			clickableSet[info.iso3Norm] ||
431			clickableSet[info.idNorm] ||
432			Boolean(getCountryProfile(info)) ||
433			isCoreCountry(info) ||
434			Boolean(getCountryLink(info))
435		);
436
437		const isPrimaryPolygonOnlyCountry = (info) => Boolean(
438			primaryPolygonOnlyTokenSet[info.nameNorm] ||
439			primaryPolygonOnlyTokenSet[info.iso2Norm] ||
440			primaryPolygonOnlyTokenSet[info.iso3Norm] ||
441			primaryPolygonOnlyTokenSet[info.idNorm]
442		);
443
444		const countryInfoById = {};
445		const countryByToken = {};
446		countries.forEach((country) => {
447			const info = getCountryInfo(country, countryMetaById);
448			countryInfoById[country.id] = info;
449			[info.nameNorm, info.iso2Norm, info.iso3Norm, info.idNorm].forEach((token) => {
450				if (token && !countryByToken[token]) {
451					countryByToken[token] = country;
452				}
453			});
454		});
455
456		const virtualCountryByFocusId = {};
457		const virtualCountryByToken = {};
458		const addVirtualCountryFromToken = (token) => {
459			const normalized = normalizeToken(token);
460			if (!normalized || countryByToken[normalized]) {
461				return;
462			}
463
464			const fallbackInfo = fallbackCountryByToken[normalized];
465			if (!fallbackInfo || !Array.isArray(fallbackInfo.centroid)) {
466				return;
467			}
468
469			const focusId = `virtual-${fallbackInfo.idNorm || fallbackInfo.iso2Norm || fallbackInfo.nameNorm || normalized}`;
470			if (!virtualCountryByFocusId[focusId]) {
471				virtualCountryByFocusId[focusId] = Object.assign({}, fallbackInfo, { focusId });
472			}
473
474			const info = virtualCountryByFocusId[focusId];
475			[info.nameNorm, info.iso2Norm, info.iso3Norm, info.idNorm].forEach((value) => {
476				if (value) {
477					virtualCountryByToken[value] = info;
478				}
479			});
480		};
481
482		Object.keys(clickableSet).forEach(addVirtualCountryFromToken);
483		Object.keys(profileByKey).forEach(addVirtualCountryFromToken);
484		Object.keys(linksByKey).forEach(addVirtualCountryFromToken);
485		if (coreCountryToken) {
486			addVirtualCountryFromToken(coreCountryToken);
487		}
488
489		const virtualCountries = Object.keys(virtualCountryByFocusId)
490			.map((key) => virtualCountryByFocusId[key])
491			.filter((info) => isClickableCountry(info) || isCoreCountry(info));
492
493		const findCountryTargetByToken = (token) => {
494			const normalized = normalizeToken(token);
495			if (!normalized) {
496				return null;
497			}
498
499			const country = countryByToken[normalized];
500			if (country) {
501				return {
502					type: 'country',
503					focusId: country.id,
504					country,
505					info: countryInfoById[country.id]
506				};
507			}
508
509			const virtualInfo = virtualCountryByToken[normalized];
510			if (virtualInfo) {
511				return {
512					type: 'virtual',
513					focusId: virtualInfo.focusId,
514					country: null,
515					info: virtualInfo
516				};
517			}
518
519			return null;
520		};
521
522		let primaryPolygonFeatureByCountryId = {};
523		const getTargetCentroid = (target) => {
524			if (!target) {
525				return null;
526			}
527
528			if (target.type === 'virtual') {
529				const centroid = target.info && Array.isArray(target.info.centroid) ? target.info.centroid : null;
530				if (centroid && isFinite(centroid[0]) && isFinite(centroid[1])) {
531					return centroid;
532				}
533				return null;
534			}
535
536			if (!target.country) {
537				return null;
538			}
539
540			const primaryPolygon = primaryPolygonFeatureByCountryId[target.country.id];
541			const centroid = d3.geoCentroid(primaryPolygon || target.country);
542			if (!centroid || !isFinite(centroid[0]) || !isFinite(centroid[1])) {
543				return null;
544			}
545			return centroid;
546		};
547
548		const stateFeatureByCountryId = {};
549		countries.forEach((country) => {
550			const info = countryInfoById[country.id];
551			const stateToken = getStateTokenFromProfile(info);
552			if (!stateToken) {
553				return;
554			}
555
556			const countryTokens = {};
557			[countryInfoById[country.id].iso2Norm, countryInfoById[country.id].iso3Norm, countryInfoById[country.id].nameNorm, countryInfoById[country.id].idNorm]
558				.forEach((token) => {
559					if (token) {
560						countryTokens[token] = true;
561					}
562				});
563
564			for (let i = 0; i < admin1Features.length; i += 1) {
565				const feature = admin1Features[i];
566				const props = feature && feature.properties ? feature.properties : {};
567				const adminCountryTokens = getAdmin1CountryTokens(props);
568				const hasCountryMatch = Object.keys(countryTokens).some((token) => adminCountryTokens[token]);
569				if (!hasCountryMatch) {
570					continue;
571				}
572
573				const adminStateTokens = getAdmin1StateTokens(props);
574				if (adminStateTokens[stateToken]) {
575					stateFeatureByCountryId[country.id] = feature;
576					break;
577				}
578			}
579		});
580
581		const highlightedCountries = countries.filter((country) => isClickableCountry(countryInfoById[country.id]));
582		const highlightedVirtualCountries = virtualCountries.filter((info) => isClickableCountry(info));
583		primaryPolygonFeatureByCountryId = {};
584		highlightedCountries.forEach((country) => {
585			const info = countryInfoById[country.id];
586			if (!isPrimaryPolygonOnlyCountry(info)) {
587				return;
588			}
589
590			const geometry = country && country.geometry ? country.geometry : null;
591			if (!geometry || geometry.type !== 'MultiPolygon' || !Array.isArray(geometry.coordinates)) {
592				return;
593			}
594
595			let largestPolygon = null;
596			let largestArea = 0;
597			geometry.coordinates.forEach((polygonCoords) => {
598				const polygonFeature = {
599					type: 'Feature',
600					id: country.id,
601					properties: country.properties || {},
602					geometry: {
603						type: 'Polygon',
604						coordinates: polygonCoords
605					}
606				};
607				const area = Math.abs(d3.geoArea(polygonFeature));
608				if (area > largestArea) {
609					largestArea = area;
610					largestPolygon = polygonFeature;
611				}
612			});
613
614			if (largestPolygon) {
615				primaryPolygonFeatureByCountryId[country.id] = largestPolygon;
616			}
617		});
618		const primaryClickableCountries = highlightedCountries
619			.map((country) => {
620				// Keep admin1 (state/province) rendering authoritative when it is available.
621				if (stateFeatureByCountryId[country.id]) {
622					return null;
623				}
624
625				const feature = primaryPolygonFeatureByCountryId[country.id];
626				if (!feature) {
627					return null;
628				}
629				const centroid = d3.geoCentroid(feature);
630				if (!centroid || !isFinite(centroid[0]) || !isFinite(centroid[1])) {
631					return null;
632				}
633				return {
634					country,
635					info: countryInfoById[country.id],
636					feature,
637					centroid
638				};
639			})
640			.filter((item) => item !== null);
641
642		const projection = d3.geoOrthographic()
643			.scale(Math.min(width, height) * 0.47)
644			.translate([width / 2, height / 2])
645			.rotate([0, -15]);
646
647		if (config.coreCountry) {
648			const coreCountryTarget = findCountryTargetByToken(config.coreCountry);
649			const centroid = getTargetCentroid(coreCountryTarget);
650			if (centroid) {
651				projection.rotate([-centroid[0], -centroid[1]]);
652			}
653		}
654
655		const path = d3.geoPath().projection(projection);
656		const dragSensitivity = 60;
657		let hoveredCountryId = null;
658		let rotationTimer = null;
659		let rotationAnimationFrame = null;
660		let focusedCountryId = null;
661		let focusedCentroid = null;
662		let preFocusRotation = null;
663		let isFocusTooltipVisible = false;
664		let suppressCloseOnScrollUntil = 0;
665		let focusRequestId = 0;
666		let isWidgetVisible = true;
667
668		const sectionElement = widget.closest('section[style*="background-color"]')
669			|| widget.closest('section')
670			|| widget.parentElement
671			|| widget;
672		const sectionComputedStyle = window.getComputedStyle(sectionElement);
673		if (sectionComputedStyle.position === 'static') {
674			sectionElement.style.position = 'relative';
675		}
676
677		const suppressCloseOnScroll = (durationMs) => {
678			const duration = Number(durationMs) || 0;
679			suppressCloseOnScrollUntil = Date.now() + Math.max(duration, 0);
680		};
681
682		const isScrollCloseSuppressed = () => Date.now() < suppressCloseOnScrollUntil;
683
684		const isCurrentFocusCountry = (countryId) => (
685			focusedCountryId !== null &&
686			countryId != null &&
687			String(focusedCountryId) === String(countryId)
688		);
689
690		const shouldApplyFocusedAvailableColor = () => (
691			focusedCountryId !== null && isFocusTooltipVisible
692		);
693
694		const isSectionRealignmentNeeded = () => {
695			const viewportHeight = Math.round(window.innerHeight || document.documentElement.clientHeight || 0);
696			if (!viewportHeight) {
697				return true;
698			}
699
700			const rect = sectionElement.getBoundingClientRect();
701			const intersectsViewport = rect.bottom > 0 && rect.top < viewportHeight;
702			if (!intersectsViewport) {
703				return true;
704			}
705
706			if (rect.height > viewportHeight) {
707				return Math.abs(rect.bottom - viewportHeight) > 8;
708			}
709
710			return Math.abs(rect.bottom - viewportHeight) > 8;
711		};
712
713		const scrollSectionIntoFocus = (requestId) => new Promise((resolve) => {
714			if (!isMobileOrTabletViewport()) {
715				resolve();
716				return;
717			}
718
719			if (!isSectionRealignmentNeeded()) {
720				resolve();
721				return;
722			}
723
724			const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
725			suppressCloseOnScroll(SECTION_SCROLL_TIMEOUT + ROTATION_DURATION);
726			sectionElement.scrollIntoView({
727				behavior: reducedMotion ? 'auto' : 'smooth',
728				block: 'end',
729				inline: 'nearest'
730			});
731
732			if (reducedMotion) {
733				window.requestAnimationFrame(resolve);
734				return;
735			}
736
737			const startedAt = Date.now();
738			let stableFrameCount = 0;
739				const waitForAlignment = () => {
740					if (requestId !== focusRequestId) {
741						resolve();
742						return;
743					}
744
745					const rect = sectionElement.getBoundingClientRect();
746					const viewportHeight = Math.round(window.innerHeight || document.documentElement.clientHeight || 0);
747					const aligned = viewportHeight > 0
748						? Math.abs(rect.bottom - viewportHeight) <= 4
749						: true;
750					if (aligned) {
751						stableFrameCount += 1;
752					} else {
753						stableFrameCount = 0;
754					}
755
756				if (stableFrameCount >= 3 || (Date.now() - startedAt) >= SECTION_SCROLL_TIMEOUT) {
757					resolve();
758					return;
759				}
760
761				window.requestAnimationFrame(waitForAlignment);
762			};
763
764			window.requestAnimationFrame(waitForAlignment);
765		});
766
767		widget.innerHTML = '<div class="ziptrak-globe-widget__tooltip-line" hidden></div>';
768		const tooltipLine = widget.querySelector('.ziptrak-globe-widget__tooltip-line');
769		const syncedTooltip = document.createElement('div');
770		syncedTooltip.className = 'ziptrak-globe-widget__tooltip';
771		syncedTooltip.hidden = true;
772		syncedTooltip.style.position = 'absolute';
773		syncedTooltip.style.zIndex = '9';
774		sectionElement.appendChild(syncedTooltip);
775
776		const svg = d3.select(widget)
777			.append('svg')
778			.attr('class', 'ziptrak-globe-widget__svg')
779			.attr('width', width)
780			.attr('height', height)
781			.attr('viewBox', `0 0 ${width} ${height}`);
782
783		svg.append('path')
784			.datum({ type: 'Sphere' })
785			.attr('class', 'ziptrak-globe-widget__sphere')
786			.attr('d', path)
787			.attr('fill', config.oceanColor);
788
789		const countriesGroup = svg.append('g').attr('class', 'ziptrak-globe-widget__countries');
790		const bordersPath = svg.append('path')
791			.datum(borders)
792			.attr('class', 'ziptrak-globe-widget__borders')
793			.attr('fill', 'none')
794			.attr('stroke', config.borderColor)
795			.attr('stroke-width', 0.8)
796			.attr('d', path);
797
798		const updateTooltipPosition = () => {
799			if (!syncedTooltip || syncedTooltip.hidden) {
800				return;
801			}
802
803			const sectionRect = sectionElement.getBoundingClientRect();
804			const widgetRect = widget.getBoundingClientRect();
805			const tooltipRect = syncedTooltip.getBoundingClientRect();
806			const viewportWidth = Math.round(window.innerWidth || document.documentElement.clientWidth || 0);
807			const viewportHeight = Math.round(window.innerHeight || document.documentElement.clientHeight || 0);
808			const maxLeft = Math.max(TOOLTIP_EDGE_OFFSET, sectionRect.width - tooltipRect.width - TOOLTIP_EDGE_OFFSET);
809			let top = TOOLTIP_EDGE_OFFSET;
810			let left = TOOLTIP_EDGE_OFFSET;
811
812			if (isMobileOrTabletViewport()) {
813				syncedTooltip.classList.add('ziptrak-globe-widget__tooltip--mobile');
814				const targetTopInViewport = Math.max(
815					TOOLTIP_EDGE_OFFSET,
816					viewportHeight - tooltipRect.height - TOOLTIP_BOTTOM_OFFSET
817				);
818				const targetLeftInViewport = ((viewportWidth - tooltipRect.width) / 2);
819				top = targetTopInViewport - sectionRect.top;
820				left = targetLeftInViewport - sectionRect.left;
821			} else {
822				syncedTooltip.classList.remove('ziptrak-globe-widget__tooltip--mobile');
823				const targetTopInViewport = widgetRect.top + ((widgetRect.height - tooltipRect.height) / 2);
824				const targetLeftInViewport = widgetRect.right - tooltipRect.width - TOOLTIP_RIGHT_OFFSET;
825				top = targetTopInViewport - sectionRect.top;
826				left = targetLeftInViewport - sectionRect.left;
827			}
828
829			syncedTooltip.style.top = `${Math.max(TOOLTIP_EDGE_OFFSET, top)}px`;
830			syncedTooltip.style.left = `${clamp(left, TOOLTIP_EDGE_OFFSET, maxLeft)}px`;
831			syncedTooltip.style.transform = 'none';
832			syncedTooltip.style.bottom = 'auto';
833			syncedTooltip.style.right = 'auto';
834		};
835
836		const stopRotationAnimation = () => {
837			if (rotationAnimationFrame) {
838				window.cancelAnimationFrame(rotationAnimationFrame);
839				rotationAnimationFrame = null;
840			}
841		};
842
843		const updateTooltipLinePosition = () => {
844			if (!syncedTooltip || !tooltipLine || !focusedCentroid || focusedCountryId === null || !isFocusTooltipVisible || syncedTooltip.hidden) {
845				return;
846			}
847
848			if (isMobileOrTabletViewport()) {
849				tooltipLine.hidden = true;
850				return;
851			}
852
853			const point = projection(focusedCentroid);
854			if (!point) {
855				tooltipLine.hidden = true;
856				return;
857			}
858
859			const widgetRect = widget.getBoundingClientRect();
860			const tooltipRect = syncedTooltip.getBoundingClientRect();
861			const x1 = point[0];
862			const y1 = point[1];
863			const x2 = (tooltipRect.left - widgetRect.left) + (tooltipRect.width / 2);
864			const y2 = (tooltipRect.top - widgetRect.top) + tooltipRect.height;
865			const dx = x2 - x1;
866			const dy = y2 - y1;
867			const length = Math.max(Math.sqrt((dx * dx) + (dy * dy)), 2);
868			const angle = Math.atan2(dy, dx);
869
870			tooltipLine.hidden = false;
871			tooltipLine.style.left = `${x1}px`;
872			tooltipLine.style.top = `${y1}px`;
873			tooltipLine.style.width = `${length}px`;
874			tooltipLine.style.transform = `rotate(${angle}rad)`;
875		};
876
877		const hideFocusTooltip = () => {
878			isFocusTooltipVisible = false;
879			if (syncedTooltip) {
880				syncedTooltip.hidden = true;
881			}
882			if (tooltipLine) {
883				tooltipLine.hidden = true;
884			}
885		};
886
887		const showFocusTooltip = (info) => {
888			if (!syncedTooltip) {
889				return;
890			}
891
892			const profile = getCountryProfile(info);
893			const state = profile && profile.state ? String(profile.state) : '';
894			const description = profile && profile.description ? String(profile.description) : COUNTRY_DESCRIPTION;
895			const title = escapeHtml(info.name);
896			const stateHtml = state ? `<p class=\"ziptrak-globe-widget__tooltip-state\">${escapeHtml(state)}</p>` : '';
897			const text = escapeHtml(description).replace(/\n/g, '<br>');
898			syncedTooltip.innerHTML = `<button type=\"button\" class=\"ziptrak-globe-widget__tooltip-close\" aria-label=\"Close\">&times;</button><h4 class=\"ziptrak-globe-widget__tooltip-title\">${title}</h4>${stateHtml}<p class=\"ziptrak-globe-widget__tooltip-description\">${text}</p>`;
899			syncedTooltip.hidden = false;
900			isFocusTooltipVisible = true;
901			refreshInteractiveFills();
902			suppressCloseOnScroll(AUTO_SCROLL_SUPPRESS_DURATION);
903			updateTooltipPosition();
904
905			const closeButton = syncedTooltip.querySelector('.ziptrak-globe-widget__tooltip-close');
906			if (closeButton) {
907				closeButton.addEventListener('click', (event) => {
908					event.preventDefault();
909					event.stopPropagation();
910					closeFocusCountry();
911				});
912			}
913
914			updateTooltipLinePosition();
915			window.requestAnimationFrame(() => {
916				if (!syncedTooltip.hidden) {
917					updateTooltipPosition();
918					updateTooltipLinePosition();
919				}
920			});
921		};
922
923		const fillForCountry = (country) => {
924			const info = countryInfoById[country.id];
925			if (stateFeatureByCountryId[country.id]) {
926				return config.landColor;
927			}
928			if (primaryPolygonFeatureByCountryId[country.id]) {
929				return config.landColor;
930			}
931			if (isClickableCountry(info) && shouldApplyFocusedAvailableColor()) {
932				return isCurrentFocusCountry(country.id) ? config.clickableColor : config.focusedAvailableColor;
933			}
934			if (hoveredCountryId === country.id && isClickableCountry(info)) {
935				return config.clickableHoverColor;
936			}
937			if (isClickableCountry(info)) {
938				return config.clickableColor;
939			}
940			return config.landColor;
941		};
942
943		const highlightedStates = highlightedCountries
944			.map((country) => {
945				const stateFeature = stateFeatureByCountryId[country.id];
946				if (!stateFeature) {
947					return null;
948				}
949				const centroid = d3.geoCentroid(stateFeature);
950				if (!centroid || !isFinite(centroid[0]) || !isFinite(centroid[1])) {
951					return null;
952				}
953				return {
954					country,
955					info: countryInfoById[country.id],
956					focusId: country.id,
957					feature: stateFeature,
958					centroid
959				};
960			})
961			.filter((item) => item !== null);
962		const highlightedStateCentroidByCountryId = {};
963		highlightedStates.forEach((item) => {
964			highlightedStateCentroidByCountryId[item.country.id] = item.centroid;
965		});
966
967		const fillForState = (datum) => {
968			if (shouldApplyFocusedAvailableColor()) {
969				return isCurrentFocusCountry(datum.country.id) ? config.clickableColor : config.focusedAvailableColor;
970			}
971
972			return hoveredCountryId === datum.country.id ? config.clickableHoverColor : config.clickableColor;
973		};
974		const fillForPrimaryCountry = (datum) => {
975			if (shouldApplyFocusedAvailableColor()) {
976				return isCurrentFocusCountry(datum.country.id) ? config.clickableColor : config.focusedAvailableColor;
977			}
978
979			return hoveredCountryId === datum.country.id ? config.clickableHoverColor : config.clickableColor;
980		};
981
982		let countryPaths = null;
983		let statePaths = null;
984		let primaryCountryPaths = null;
985
986		const refreshInteractiveFills = () => {
987			if (countryPaths) {
988				countryPaths.attr('fill', (item) => fillForCountry(item));
989			}
990			if (statePaths) {
991				statePaths.attr('fill', (item) => fillForState(item));
992			}
993			if (primaryCountryPaths) {
994				primaryCountryPaths.attr('fill', (item) => fillForPrimaryCountry(item));
995			}
996		};
997
998		const clearHoverState = () => {
999			if (focusedCountryId !== null) {
1000				return;
1001			}
1002			hoveredCountryId = null;
1003			refreshInteractiveFills();
1004			svg.style('cursor', 'default');
1005			hideFocusTooltip();
1006		};
1007
1008		const setHoverState = (countryId, cursor = 'pointer') => {
1009			if (focusedCountryId !== null) {
1010				return;
1011			}
1012			hoveredCountryId = countryId;
1013			refreshInteractiveFills();
1014			svg.style('cursor', cursor);
1015		};
1016
1017		countryPaths = countriesGroup
1018			.selectAll('path')
1019			.data(countries)
1020			.enter()
1021			.append('path')
1022			.attr('class', 'ziptrak-globe-widget__country')
1023			.attr('d', path)
1024			.attr('fill', (country) => fillForCountry(country))
1025			.on('mousemove', (country) => {
1026				const info = countryInfoById[country.id];
1027				if (primaryPolygonFeatureByCountryId[country.id] && !stateFeatureByCountryId[country.id]) {
1028					clearHoverState();
1029					return;
1030				}
1031
1032				const isClickable = isClickableCountry(info);
1033				setHoverState(country.id, isClickable ? 'pointer' : 'default');
1034			})
1035			.on('mouseleave', clearHoverState)
1036			.on('click', (country) => {
1037				const info = countryInfoById[country.id];
1038				if (primaryPolygonFeatureByCountryId[country.id] && !stateFeatureByCountryId[country.id]) {
1039					return;
1040				}
1041				if (!isClickableCountry(info)) {
1042					return;
1043				}
1044				const centroid = highlightedStateCentroidByCountryId[country.id] || d3.geoCentroid(country);
1045				focusCountry(country.id, info, centroid);
1046			});
1047
1048		const statesGroup = svg.append('g').attr('class', 'ziptrak-globe-widget__states');
1049		statePaths = statesGroup
1050			.selectAll('path')
1051			.data(highlightedStates)
1052			.enter()
1053			.append('path')
1054			.attr('class', 'ziptrak-globe-widget__state')
1055			.attr('d', (datum) => path(datum.feature))
1056			.attr('fill', (datum) => fillForState(datum))
1057			.on('mousemove', (datum) => {
1058				setHoverState(datum.country.id, 'pointer');
1059			})
1060			.on('mouseleave', clearHoverState)
1061			.on('click', (datum) => {
1062				if (!datum.info) {
1063					return;
1064				}
1065				focusCountry(datum.focusId, datum.info, datum.centroid);
1066			});
1067
1068		const primaryCountriesGroup = svg.append('g').attr('class', 'ziptrak-globe-widget__primary-countries');
1069		primaryCountryPaths = primaryCountriesGroup
1070			.selectAll('path')
1071			.data(primaryClickableCountries)
1072			.enter()
1073			.append('path')
1074			.attr('class', 'ziptrak-globe-widget__country ziptrak-globe-widget__country--primary')
1075			.attr('d', (datum) => path(datum.feature))
1076			.attr('fill', (datum) => fillForPrimaryCountry(datum))
1077			.on('mousemove', (datum) => {
1078				setHoverState(datum.country.id, 'pointer');
1079			})
1080			.on('mouseleave', clearHoverState)
1081			.on('click', (datum) => {
1082				focusCountry(datum.country.id, datum.info, datum.centroid);
1083			});
1084
1085		const pulseGroup = svg.append('g').attr('class', 'ziptrak-globe-widget__pulses');
1086		const pulseTargets = [];
1087		highlightedCountries.forEach((country) => {
1088			const info = countryInfoById[country.id];
1089			const primaryPolygon = primaryPolygonFeatureByCountryId[country.id];
1090			const centroid = highlightedStateCentroidByCountryId[country.id]
1091				|| (primaryPolygon ? d3.geoCentroid(primaryPolygon) : d3.geoCentroid(country));
1092			if (!info || !centroid || !isFinite(centroid[0]) || !isFinite(centroid[1])) {
1093				return;
1094			}
1095
1096			pulseTargets.push({
1097				focusId: country.id,
1098				info,
1099				centroid
1100			});
1101		});
1102
1103		highlightedVirtualCountries.forEach((info) => {
1104			const centroid = Array.isArray(info.centroid) ? info.centroid : null;
1105			if (!centroid || !isFinite(centroid[0]) || !isFinite(centroid[1])) {
1106				return;
1107			}
1108
1109			pulseTargets.push({
1110				focusId: info.focusId,
1111				info,
1112				centroid
1113			});
1114		});
1115
1116		const handlePulseClick = (datum) => {
1117			if (!isFrontHemisphere(projection, datum.centroid)) {
1118				return;
1119			}
1120			d3.event.preventDefault();
1121			d3.event.stopPropagation();
1122			focusCountry(datum.focusId, datum.info, datum.centroid);
1123		};
1124
1125		pulseTargets.forEach((target) => {
1126			const point = projection(target.centroid);
1127			if (!point) {
1128				return;
1129			}
1130
1131			pulseGroup.append('circle')
1132				.attr('class', 'ziptrak-globe-widget__pulse-core')
1133				.datum(target)
1134				.attr('cx', point[0])
1135				.attr('cy', point[1])
1136				.attr('r', 3)
1137				.attr('fill', '#fff')
1138				.style('cursor', 'pointer')
1139				.on('click', handlePulseClick);
1140
1141			createPulse(
1142				pulseGroup.append('circle')
1143					.attr('class', 'ziptrak-globe-widget__pulse-ring')
1144					.datum(target)
1145					.attr('cx', point[0])
1146					.attr('cy', point[1])
1147					.style('cursor', 'pointer')
1148					.on('click', handlePulseClick)
1149			);
1150		});
1151
1152		const updateProjection = () => {
1153			svg.select('.ziptrak-globe-widget__sphere').attr('d', path);
1154			countryPaths.attr('d', path);
1155			bordersPath.attr('d', path);
1156			statePaths
1157				.attr('d', (datum) => path(datum.feature))
1158				.each(function updateState(datum) {
1159					const isFront = isFrontHemisphere(projection, datum.centroid);
1160					d3.select(this)
1161						.attr('display', isFront ? null : 'none')
1162						.style('pointer-events', isFront ? 'auto' : 'none');
1163				});
1164			primaryCountryPaths
1165				.attr('d', (datum) => path(datum.feature))
1166				.each(function updatePrimaryCountry(datum) {
1167					const isFront = isFrontHemisphere(projection, datum.centroid);
1168					d3.select(this)
1169						.attr('display', isFront ? null : 'none')
1170						.style('pointer-events', isFront ? 'auto' : 'none');
1171				});
1172
1173			pulseGroup.selectAll('.ziptrak-globe-widget__pulse-core').each(function updateCore(datum) {
1174				const point = projection(datum.centroid);
1175				const isFront = isFrontHemisphere(projection, datum.centroid);
1176				const opacity = isFront ? 1 : backsideDotOpacity;
1177				if (!point) {
1178					d3.select(this).attr('display', 'none');
1179					return;
1180				}
1181				d3.select(this)
1182					.attr('display', null)
1183					.attr('cx', point[0])
1184					.attr('cy', point[1])
1185					.attr('opacity', opacity)
1186					.style('pointer-events', isFront ? 'auto' : 'none')
1187					.style('cursor', isFront ? 'pointer' : 'default');
1188			});
1189
1190			pulseGroup.selectAll('.ziptrak-globe-widget__pulse-ring').each(function updateRing(datum) {
1191				const point = projection(datum.centroid);
1192				const isFront = isFrontHemisphere(projection, datum.centroid);
1193				const opacity = isFront ? 1 : backsideDotOpacity;
1194				if (!point) {
1195					d3.select(this).attr('display', 'none');
1196					return;
1197				}
1198				d3.select(this)
1199					.attr('display', null)
1200					.attr('cx', point[0])
1201					.attr('cy', point[1])
1202					.attr('opacity', opacity)
1203					.style('pointer-events', isFront ? 'auto' : 'none')
1204					.style('cursor', isFront ? 'pointer' : 'default');
1205			});
1206
1207			if (focusedCountryId !== null && isFocusTooltipVisible) {
1208				updateTooltipLinePosition();
1209			}
1210		};
1211
1212		const stopRotation = () => {
1213			if (rotationTimer) {
1214				rotationTimer.stop();
1215				rotationTimer = null;
1216			}
1217			stopRotationAnimation();
1218		};
1219
1220		const animateRotationTo = (targetRotate, duration, onDone) => {
1221			stopRotationAnimation();
1222			const startRotate = projection.rotate().slice();
1223			const interpolator = d3.interpolateArray(startRotate, targetRotate);
1224			const startTime = Date.now();
1225
1226			const tick = () => {
1227				const elapsed = Date.now() - startTime;
1228				const progress = clamp(elapsed / duration, 0, 1);
1229				const eased = d3.easeCubicInOut(progress);
1230				projection.rotate(interpolator(eased));
1231				updateProjection();
1232
1233				if (progress < 1) {
1234					rotationAnimationFrame = window.requestAnimationFrame(tick);
1235					return;
1236				}
1237
1238				rotationAnimationFrame = null;
1239				if (typeof onDone === 'function') {
1240					onDone();
1241				}
1242			};
1243
1244			rotationAnimationFrame = window.requestAnimationFrame(tick);
1245		};
1246
1247		const closeFocusCountry = () => {
1248			if (focusedCountryId === null) {
1249				return;
1250			}
1251
1252			focusRequestId += 1;
1253			const targetRotation = preFocusRotation ? preFocusRotation.slice() : projection.rotate().slice();
1254			focusedCountryId = null;
1255			focusedCentroid = null;
1256			hoveredCountryId = null;
1257			hideFocusTooltip();
1258			refreshInteractiveFills();
1259			animateRotationTo(targetRotation, ROTATION_DURATION, () => {
1260				maybeStartRotation();
1261			});
1262		};
1263
1264		const handleViewportScroll = () => {
1265			if (focusedCountryId === null) {
1266				return;
1267			}
1268
1269			if (isScrollCloseSuppressed()) {
1270				if (isFocusTooltipVisible) {
1271					updateTooltipPosition();
1272					updateTooltipLinePosition();
1273				}
1274				return;
1275			}
1276
1277			closeFocusCountry();
1278		};
1279
1280		const focusCountry = (focusId, info, centroid) => {
1281			if (!centroid || !isFinite(centroid[0]) || !isFinite(centroid[1])) {
1282				return;
1283			}
1284
1285			const requestId = focusRequestId + 1;
1286			focusRequestId = requestId;
1287			stopRotation();
1288			preFocusRotation = projection.rotate().slice();
1289			focusedCountryId = focusId;
1290			focusedCentroid = centroid;
1291			hoveredCountryId = null;
1292			hideFocusTooltip();
1293			refreshInteractiveFills();
1294
1295			scrollSectionIntoFocus(requestId).then(() => {
1296				if (requestId !== focusRequestId) {
1297					return;
1298				}
1299
1300				const targetRotate = getFocusRotateForCentroid(centroid, config.focusLonOffset, config.focusLatOffset);
1301				animateRotationTo(targetRotate, ROTATION_DURATION, () => {
1302					if (requestId !== focusRequestId) {
1303						return;
1304					}
1305
1306					showFocusTooltip(info);
1307				});
1308			});
1309		};
1310
1311		const startRotation = () => {
1312			stopRotation();
1313			if (config.rotationSpeed <= 0) {
1314				return;
1315			}
1316
1317			let lastTick = null;
1318			rotationTimer = d3.timer(() => {
1319				const now = Date.now();
1320				if (lastTick === null) {
1321					lastTick = now;
1322					return;
1323				}
1324				const delta = now - lastTick;
1325				if (delta < ROTATION_MIN_FRAME_INTERVAL) {
1326					return;
1327				}
1328				const rotate = projection.rotate();
1329				rotate[0] += (config.rotationSpeed * delta) / 40;
1330				if (rotate[0] >= 180) {
1331					rotate[0] -= 360;
1332				}
1333				projection.rotate(rotate);
1334				updateProjection();
1335				lastTick = now;
1336			});
1337		};
1338
1339		const maybeStartRotation = () => {
1340			if (focusedCountryId !== null || !isWidgetVisible) {
1341				return;
1342			}
1343			startRotation();
1344		};
1345
1346		let dragUpdateFrame = null;
1347		const scheduleDragUpdate = () => {
1348			if (dragUpdateFrame !== null) {
1349				return;
1350			}
1351			dragUpdateFrame = window.requestAnimationFrame(() => {
1352				dragUpdateFrame = null;
1353				updateProjection();
1354			});
1355		};
1356
1357		svg.call(
1358			d3.drag()
1359				.on('start', () => {
1360					if (focusedCountryId !== null) {
1361						return;
1362					}
1363					stopRotation();
1364				})
1365				.on('drag', () => {
1366					if (focusedCountryId !== null) {
1367						return;
1368					}
1369					const rotate = projection.rotate();
1370					const factor = dragSensitivity / projection.scale();
1371					projection.rotate([
1372						rotate[0] + d3.event.dx * factor,
1373						rotate[1] - d3.event.dy * factor
1374					]);
1375					scheduleDragUpdate();
1376				})
1377				.on('end', () => {
1378					if (focusedCountryId !== null) {
1379						return;
1380					}
1381					maybeStartRotation();
1382				})
1383		);
1384
1385		widget.addEventListener('click', (event) => {
1386			if (focusedCountryId === null) {
1387				return;
1388			}
1389
1390			const clickedInsideTooltip = syncedTooltip && syncedTooltip.contains(event.target);
1391			const clickedCountryPath = event.target && event.target.closest && event.target.closest('.ziptrak-globe-widget__country');
1392			const clickedStatePath = event.target && event.target.closest && event.target.closest('.ziptrak-globe-widget__state');
1393			if (clickedInsideTooltip || clickedCountryPath || clickedStatePath) {
1394				return;
1395			}
1396
1397			closeFocusCountry();
1398		});
1399
1400		let resizeFrame = null;
1401		const resize = () => {
1402			const previousWidth = width;
1403			const previousHeight = height;
1404			applyWidgetSize();
1405
1406			if (previousWidth !== width || previousHeight !== height) {
1407				svg
1408					.attr('width', width)
1409					.attr('height', height)
1410					.attr('viewBox', `0 0 ${width} ${height}`);
1411
1412				projection
1413					.scale(Math.min(width, height) * 0.47)
1414					.translate([width / 2, height / 2]);
1415				updateProjection();
1416			}
1417
1418			updateTooltipPosition();
1419			updateTooltipLinePosition();
1420		};
1421
1422		const handleResize = () => {
1423			if (resizeFrame !== null) {
1424				window.cancelAnimationFrame(resizeFrame);
1425			}
1426
1427			resizeFrame = window.requestAnimationFrame(() => {
1428				resizeFrame = null;
1429				resize();
1430			});
1431		};
1432
1433		window.addEventListener('resize', handleResize);
1434		window.addEventListener('scroll', handleViewportScroll, { passive: true });
1435		window.addEventListener('wheel', handleViewportScroll, { passive: true });
1436		window.addEventListener('touchmove', handleViewportScroll, { passive: true });
1437
1438		if (typeof window.IntersectionObserver === 'function') {
1439			const visibilityObserver = new window.IntersectionObserver((entries) => {
1440				const entry = entries[entries.length - 1];
1441				isWidgetVisible = Boolean(entry && entry.isIntersecting);
1442				if (isWidgetVisible) {
1443					maybeStartRotation();
1444				} else {
1445					stopRotation();
1446				}
1447			}, { threshold: 0 });
1448			visibilityObserver.observe(widget);
1449		}
1450
1451		updateProjection();
1452		startRotation();
1453	};
1454
1455	const initGlobes = () => {
1456		const widgets = document.querySelectorAll('.ziptrak-globe-widget[data-globe-config]');
1457		if (!widgets.length || typeof window.d3 === 'undefined' || typeof window.topojson === 'undefined') {
1458			return;
1459		}
1460
1461		getWorldData()
1462			.then((payload) => {
1463				widgets.forEach((widget) => {
1464					setupWidget(
1465						widget,
1466						payload.worldData,
1467						payload.admin1Features,
1468						payload.countryMetaById,
1469						payload.fallbackCountryByToken || {},
1470						payload.primaryPolygonOnlyTokenSet || {}
1471					);
1472				});
1473			});
1474	};
1475
1476	if (document.readyState === 'loading') {
1477		document.addEventListener('DOMContentLoaded', initGlobes);
1478	} else {
1479		initGlobes();
1480	}
1481})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.