// Replace with your Mapbox access token, and ensure it's stored securely mapboxgl.accessToken = 'pk.eyJ1IjoicGFydG5lcnN0YWNrIiwiYSI6ImNtMno5OW55MzAyNGsyaXB0YnE0NWd6NGYifQ.KPKNABnrBdGr7LhZQ-i2TA'; function isMobile() { return window.innerWidth <= 768 || window.innerHeight <= 600; } // Update the map initialization with responsive settings const map = new mapboxgl.Map({ container: 'map', center: [-40, 30], zoom: isMobile() ? 1 : 2.5, style: 'mapbox://styles/partnerstack/cm1vznnxh00d201qn3h3l1z9u', projection: 'globe', pitch: 5, bearing: -30, dragPan: !isMobile(), touchZoom: false, scrollZoom: false, }); function updateMapSize() { const width = window.innerWidth; const height = window.innerHeight; // Update zoom level based on screen size if (width <= 479) { // Mobile phones map.setZoom(0.8); map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 }); } else if (width <= 550) { // Tablets map.setZoom(1.2); map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 }); } else if (width <= 767) { // Tablets map.setZoom(1.5); map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 }); } else if (width <= 1200) { // Small laptops map.setZoom(1.9); map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 }); } else if (width <= 1400) { // Small laptops map.setZoom(1.9); map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 }); } else { // Larger screens map.setZoom(2); map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 }); } // Adjust center point based on screen aspect ratio const aspectRatio = width / height; if (aspectRatio < 1) { // Portrait map.setCenter([-40, 20]); } else { // Landscape map.setCenter([-40, 30]); } // Update dot sizes when screen size changes updateDotSizes(); } // Apply padding immediately after map initialization map.setPadding({ top: isMobile() ? 0 : 0, bottom: 0, left: 0, right: 0 }); // Remove the 'load' event listener that sets padding // Rotation settings const secondsPerRevolution = 30; const maxSpinZoom = 5; const slowSpinZoom = 3; let userInteracting = false; let spinEnabled = true; let spinning = false; function spinGlobe() { if (spinning) return; spinning = true; const zoom = map.getZoom(); if (spinEnabled && !userInteracting && zoom < maxSpinZoom) { let distancePerSecond = 360 / secondsPerRevolution; if (zoom > slowSpinZoom) { const zoomDif = (maxSpinZoom - zoom) / (maxSpinZoom - slowSpinZoom); distancePerSecond *= zoomDif; } const center = map.getCenter(); center.lng -= distancePerSecond; map.easeTo({ center, duration: 1000, easing: (n) => n, pitch: map.getPitch(), bearing: map.getBearing() }); requestAnimationFrame(() => { spinning = false; spinGlobe(); }); } else { spinning = false; } } map.on('mousedown', () => { userInteracting = true; }); map.on('mouseup', () => { userInteracting = false; spinGlobe(); }); map.on('dragend', () => { userInteracting = false; spinGlobe(); }); spinGlobe(); const baseSize = 25; const baseNewSize = 80; let size = baseSize; let newSize = baseNewSize; // Add this function to update dot sizes function updateDotSizes() { const width = window.innerWidth; if (width > 2048) { size = Math.round(baseSize * 1.5); // 50% larger for big screens newSize = Math.round(baseNewSize * 1.5); } else { size = baseSize; newSize = baseNewSize; } // Recreate the pulsing dots with new sizes map.removeImage('pulsing-dot'); map.removeImage('new-pulsing-dot'); const updatedPulsingDot = { width: size, height: size, data: new Uint8Array(size * size * 4), onAdd: pulsingDot.onAdd, render: function() { const duration = 1000; const t = (performance.now() % duration) / duration; const radius = (size / 2) * 0.3; const outerRadius = (size / 2) * 0.7 * t + radius; const context = this.context; context.clearRect(0, 0, this.width, this.height); context.beginPath(); context.arc(this.width / 2, this.height / 2, outerRadius, 0, Math.PI * 2); context.fillStyle = `rgba(255, 255, 225, ${1 - t})`; context.fill(); context.beginPath(); context.arc(this.width / 2, this.height / 2, radius, 0, Math.PI * 2); context.fillStyle = '#ffffff'; context.fill(); this.data = context.getImageData(0, 0, this.width, this.height).data; map.triggerRepaint(); return true; } }; const updatedNewItemPulsingDot = { width: newSize, height: newSize, data: new Uint8Array(newSize * newSize * 4), onAdd: newItemPulsingDot.onAdd, render: function() { const duration = 1000; const t = (performance.now() % duration) / duration; const radius = (newSize / 2) * 0.3; const outerRadius = (newSize / 2) * 0.7 * t + radius; const context = this.context; context.clearRect(0, 0, this.width, this.height); context.beginPath(); context.arc(this.width / 2, this.height / 2, outerRadius, 0, Math.PI * 2); context.fillStyle = `rgba(0, 183, 167, ${1 - t})`; context.fill(); context.beginPath(); context.arc(this.width / 2, this.height / 2, radius, 0, Math.PI * 2); context.fillStyle = '#00b7a7'; context.fill(); this.data = context.getImageData(0, 0, this.width, this.height).data; map.triggerRepaint(); return true; } }; map.addImage('pulsing-dot', updatedPulsingDot, { pixelRatio: 2 }); map.addImage('new-pulsing-dot', updatedNewItemPulsingDot, { pixelRatio: 2 }); } const pulsingDot = { width: size, height: size, data: new Uint8Array(size * size * 4), onAdd: function () { const canvas = document.createElement('canvas'); canvas.width = this.width; canvas.height = this.height; this.context = canvas.getContext('2d', { willReadFrequently: true }); }, render: function () { const duration = 1000; const t = (performance.now() % duration) / duration; const radius = (size / 2) * 0.3; const outerRadius = (size / 2) * 0.7 * t + radius; const context = this.context; context.clearRect(0, 0, this.width, this.height); context.beginPath(); context.arc(this.width / 2, this.height / 2, outerRadius, 0, Math.PI * 2); context.fillStyle = `rgba(255, 255, 225, ${1 - t})`; context.fill(); context.beginPath(); context.arc(this.width / 2, this.height / 2, radius, 0, Math.PI * 2); context.fillStyle = '#ffffff'; context.fill(); this.data = context.getImageData(0, 0, this.width, this.height).data; map.triggerRepaint(); return true; } }; const newItemPulsingDot = { width: newSize, height: newSize, data: new Uint8Array(newSize * newSize * 4), onAdd: function () { const canvas = document.createElement('canvas'); canvas.width = this.width; canvas.height = this.height; this.context = canvas.getContext('2d', { willReadFrequently: true }); }, render: function () { const duration = 1000; const t = (performance.now() % duration) / duration; const radius = (newSize / 2) * 0.3; const outerRadius = (newSize / 2) * 0.7 * t + radius; const context = this.context; context.clearRect(0, 0, this.width, this.height); context.beginPath(); context.arc(this.width / 2, this.height / 2, outerRadius, 0, Math.PI * 2); context.fillStyle = `rgba(0, 183, 167, ${1 - t})`; // #00b7a7 with opacity context.fill(); context.beginPath(); context.arc(this.width / 2, this.height / 2, radius, 0, Math.PI * 2); context.fillStyle = '#00b7a7'; context.fill(); this.data = context.getImageData(0, 0, this.width, this.height).data; map.triggerRepaint(); return true; } }; function getCountryName(countryCode) { const countryMap = { 'AF': 'Afghanistan', 'AX': 'Åland Islands', 'AL': 'Albania', 'DZ': 'Algeria', 'AS': 'American Samoa', 'AD': 'Andorra', 'AO': 'Angola', 'AI': 'Anguilla', 'AQ': 'Antarctica', 'AG': 'Antigua and Barbuda', 'AR': 'Argentina', 'AM': 'Armenia', 'AW': 'Aruba', 'AU': 'Australia', 'AT': 'Austria', 'AZ': 'Azerbaijan', 'BS': 'Bahamas', 'BH': 'Bahrain', 'BD': 'Bangladesh', 'BB': 'Barbados', 'BY': 'Belarus', 'BE': 'Belgium', 'BZ': 'Belize', 'BJ': 'Benin', 'BM': 'Bermuda', 'BT': 'Bhutan', 'BO': 'Bolivia', 'BQ': 'Bonaire, Sint Eustatius and Saba', 'BA': 'Bosnia and Herzegovina', 'BW': 'Botswana', 'BV': 'Bouvet Island', 'BR': 'Brazil', 'IO': 'British Indian Ocean Territory', 'BN': 'Brunei Darussalam', 'BG': 'Bulgaria', 'BF': 'Burkina Faso', 'BI': 'Burundi', 'CV': 'Cabo Verde', 'KH': 'Cambodia', 'CM': 'Cameroon', 'CA': 'Canada', 'KY': 'Cayman Islands', 'CF': 'Central African Republic', 'TD': 'Chad', 'CL': 'Chile', 'CN': 'China', 'CX': 'Christmas Island', 'CC': 'Cocos (Keeling) Islands', 'CO': 'Colombia', 'KM': 'Comoros', 'CG': 'Congo (Brazzaville)', 'CD': 'Congo (Kinshasa)', 'CK': 'Cook Islands', 'CR': 'Costa Rica', 'CI': "Côte d'Ivoire", 'HR': 'Croatia', 'CU': 'Cuba', 'CW': 'Curaçao', 'CY': 'Cyprus', 'CZ': 'Czech Republic', 'DK': 'Denmark', 'DJ': 'Djibouti', 'DM': 'Dominica', 'DO': 'Dominican Republic', 'EC': 'Ecuador', 'EG': 'Egypt', 'SV': 'El Salvador', 'GQ': 'Equatorial Guinea', 'ER': 'Eritrea', 'EE': 'Estonia', 'SZ': 'Eswatini', 'ET': 'Ethiopia', 'FK': 'Falkland Islands (Malvinas)', 'FO': 'Faroe Islands', 'FJ': 'Fiji', 'FI': 'Finland', 'FR': 'France', 'GF': 'French Guiana', 'PF': 'French Polynesia', 'TF': 'French Southern Territories', 'GA': 'Gabon', 'GM': 'Gambia', 'GE': 'Georgia', 'DE': 'Germany', 'GH': 'Ghana', 'GI': 'Gibraltar', 'GR': 'Greece', 'GL': 'Greenland', 'GD': 'Grenada', 'GP': 'Guadeloupe', 'GU': 'Guam', 'GT': 'Guatemala', 'GG': 'Guernsey', 'GN': 'Guinea', 'GW': 'Guinea-Bissau', 'GY': 'Guyana', 'HT': 'Haiti', 'HM': 'Heard Island and McDonald Islands', 'VA': 'Holy See (Vatican City State)', 'HN': 'Honduras', 'HK': 'Hong Kong', 'HU': 'Hungary', 'IS': 'Iceland', 'IN': 'India', 'ID': 'Indonesia', 'IR': 'Iran (Islamic Republic of)', 'IQ': 'Iraq', 'IE': 'Ireland', 'IM': 'Isle of Man', 'IL': 'Israel', 'IT': 'Italy', 'JM': 'Jamaica', 'JP': 'Japan', 'JE': 'Jersey', 'JO': 'Jordan', 'KZ': 'Kazakhstan', 'KE': 'Kenya', 'KI': 'Kiribati', 'KP': "Korea (Democratic People's Republic of)", 'KR': 'Korea (Republic of)', 'KW': 'Kuwait', 'KG': 'Kyrgyzstan', 'LA': "Lao People's Democratic Republic", 'LV': 'Latvia', 'LB': 'Lebanon', 'LS': 'Lesotho', 'LR': 'Liberia', 'LY': 'Libya', 'LI': 'Liechtenstein', 'LT': 'Lithuania', 'LU': 'Luxembourg', 'MO': 'Macao', 'MG': 'Madagascar', 'MW': 'Malawi', 'MY': 'Malaysia', 'MV': 'Maldives', 'ML': 'Mali', 'MT': 'Malta', 'MH': 'Marshall Islands', 'MQ': 'Martinique', 'MR': 'Mauritania', 'MU': 'Mauritius', 'YT': 'Mayotte', 'MX': 'Mexico', 'FM': 'Micronesia (Federated States of)', 'MD': 'Moldova (Republic of)', 'MC': 'Monaco', 'MN': 'Mongolia', 'ME': 'Montenegro', 'MS': 'Montserrat', 'MA': 'Morocco', 'MZ': 'Mozambique', 'MM': 'Myanmar', 'NA': 'Namibia', 'NR': 'Nauru', 'NP': 'Nepal', 'NL': 'Netherlands', 'NC': 'New Caledonia', 'NZ': 'New Zealand', 'NI': 'Nicaragua', 'NE': 'Niger', 'NG': 'Nigeria', 'NU': 'Niue', 'NF': 'Norfolk Island', 'MK': 'North Macedonia', 'MP': 'Northern Mariana Islands', 'NO': 'Norway', 'OM': 'Oman', 'PK': 'Pakistan', 'PW': 'Palau', 'PS': 'Palestine, State of', 'PA': 'Panama', 'PG': 'Papua New Guinea', 'PY': 'Paraguay', 'PE': 'Peru', 'PH': 'Philippines', 'PN': 'Pitcairn', 'PL': 'Poland', 'PT': 'Portugal', 'PR': 'Puerto Rico', 'QA': 'Qatar', 'RE': 'Réunion', 'RO': 'Romania', 'RU': 'Russian Federation', 'RW': 'Rwanda', 'BL': 'Saint Barthélemy', 'SH': 'Saint Helena, Ascension and Tristan da Cunha', 'KN': 'Saint Kitts and Nevis', 'LC': 'Saint Lucia', 'MF': 'Saint Martin (French part)', 'PM': 'Saint Pierre and Miquelon', 'VC': 'Saint Vincent and the Grenadines', 'WS': 'Samoa', 'SM': 'San Marino', 'ST': 'Sao Tome and Principe', 'SA': 'Saudi Arabia', 'SN': 'Senegal', 'RS': 'Serbia', 'SC': 'Seychelles', 'SL': 'Sierra Leone', 'SG': 'Singapore', 'SX': 'Sint Maarten (Dutch part)', 'SK': 'Slovakia', 'SI': 'Slovenia', 'SB': 'Solomon Islands', 'SO': 'Somalia', 'ZA': 'South Africa', 'GS': 'South Georgia and the South Sandwich Islands', 'SS': 'South Sudan', 'ES': 'Spain', 'LK': 'Sri Lanka', 'SD': 'Sudan', 'SR': 'Suriname', 'SJ': 'Svalbard and Jan Mayen', 'SE': 'Sweden', 'CH': 'Switzerland', 'SY': 'Syrian Arab Republic', 'TW': 'Taiwan, Province of China', 'TJ': 'Tajikistan', 'TZ': 'Tanzania, United Republic of', 'TH': 'Thailand', 'TL': 'Timor-Leste', 'TG': 'Togo', 'TK': 'Tokelau', 'TO': 'Tonga', 'TT': 'Trinidad and Tobago', 'TN': 'Tunisia', 'TR': 'Turkey', 'TM': 'Turkmenistan', 'TC': 'Turks and Caicos Islands', 'TV': 'Tuvalu', 'UG': 'Uganda', 'UA': 'Ukraine', 'AE': 'United Arab Emirates', 'GB': 'United Kingdom', 'US': 'United States', 'UM': 'United States Minor Outlying Islands', 'UY': 'Uruguay', 'UZ': 'Uzbekistan', 'VU': 'Vanuatu', 'VE': 'Venezuela (Bolivarian Republic of)', 'VN': 'Vietnam', 'VG': 'Virgin Islands (British)', 'VI': 'Virgin Islands (U.S.)', 'WF': 'Wallis and Futuna', 'EH': 'Western Sahara', 'YE': 'Yemen', 'ZM': 'Zambia', 'ZW': 'Zimbabwe' }; return countryMap[countryCode] || countryCode; } let currentPopup = null; let popupTimeout = null; let lastPopupTime = 0; function closePopup() { if (currentPopup) { currentPopup.getElement().classList.remove('show'); setTimeout(() => { if (currentPopup) { currentPopup.remove(); currentPopup = null; } }, 500); // 500ms for fade-out transition } if (popupTimeout) { clearTimeout(popupTimeout); popupTimeout = null; } } // Ensure showPopup is defined before addNextVisibleLocation function showPopup(location, coordinates) { // Check URL to disable all popups if (window.location.href.includes('globe-no-popups')) return; const now = Date.now(); if (now - lastPopupTime < 2000) return; // Prevent popups from appearing too quickly const bounds = map.getBounds(); if (!bounds.contains(coordinates)) return; closePopup(); // Close any existing popup before showing a new one const formattedAmount = `$${parseFloat(location.transactionAmount).toFixed(2)}`; // Determine popup content based on screen width const width = window.innerWidth; let popupContent; if (width > 3000) { popupContent = `
${location.companyType} Software
COMMISSION
${formattedAmount}
REGION
${getCountryName(location.countryCode)}
`; } else { popupContent = `
${location.companyType} Software
COMMISSION
${formattedAmount}
REGION
${getCountryName(location.countryCode)}
`; } // Calculate the position 3px above the dot const popupCoordinates = [coordinates[0], coordinates[1]]; const pixelCoordinates = map.project(popupCoordinates); pixelCoordinates.y -= 6; // Move 3 pixels up // Add x-offset for large screens if (window.innerWidth > 3000) { pixelCoordinates.x -= 120; // Adjust this value as needed for large screens } else { pixelCoordinates.x += 0; // Default offset for smaller screens } const adjustedCoordinates = map.unproject(pixelCoordinates); currentPopup = new mapboxgl.Popup({ closeButton: false, closeOnClick: false, className: 'custom-popup no-arrow', anchor: 'bottom' // Anchor the popup to the bottom }) .setLngLat(adjustedCoordinates) .setHTML(popupContent) .addTo(map); // Use requestAnimationFrame for a smoother fade-in effect requestAnimationFrame(() => { const popupElement = currentPopup.getElement(); popupElement.style.opacity = '0'; popupElement.style.transition = 'opacity 0.3s ease-in-out'; requestAnimationFrame(() => { popupElement.style.opacity = '1'; }); }); lastPopupTime = now; // Set a timeout to close the popup after 2 seconds popupTimeout = setTimeout(closePopup, 2000); } let isPageVisible = true; let popupInterval; let pointInterval; // Update the handleVisibilityChange function function handleVisibilityChange() { if (document.hidden) { isPageVisible = false; closePopup(); clearInterval(popupInterval); // Remove the clearInterval for pointInterval } else { isPageVisible = true; lastPopupTime = Date.now(); startPopupInterval(); // Remove the call to startPointInterval() } } function startAddingLocations() { updateVisibleLocations(); addInitialDots(); setInterval(updateVisibleLocations, 1000); // Call every second startPopupInterval(); } // Update the startPopupInterval function function startPopupInterval() { // Check URL to disable popup intervals if (window.location.href.includes('globe-no-popups')) return; if (popupInterval) { clearInterval(popupInterval); } popupInterval = setInterval(() => { if (isPageVisible) { addNextVisibleLocation(true); // Pass true to show popup } }, 3000); } // Add this new function function startPointInterval() { if (pointInterval) { clearInterval(pointInterval); } pointInterval = setInterval(() => { if (isPageVisible) { addNextVisibleLocation(false); // Pass false to not show popup } }, 500); } let popupCount = 0; // Add this at the top of your file, with other global variables function addNextVisibleLocation(shouldShowPopup = true) { if (visibleLocations.length === 0) return; if (globalThis.isFallbackMode) shouldShowPopup = false; // Check URL to disable popups if (window.location.href.includes('globe-no-popups')) shouldShowPopup = false; const bounds = map.getBounds(); let location = null; // Special logic for the first two popups if (popupCount < 2) { const minAmount = 10; // Set minimum amount to $10 for all popups const maxAmount = popupCount === 0 ? 20 : 300; for (const loc of visibleLocations) { const amount = parseFloat(loc.transactionAmount); if (bounds.contains(loc.coordinates) && amount >= minAmount && amount <= maxAmount) { location = loc; break; } } // If no location found in the desired range, fall back to any visible location with amount >= $10 if (!location) { location = visibleLocations.find(loc => bounds.contains(loc.coordinates) && parseFloat(loc.transactionAmount) >= 10 ); } popupCount++; } else { // Modified selection logic for subsequent popups const visibleInBounds = visibleLocations.filter(loc => bounds.contains(loc.coordinates) && parseFloat(loc.transactionAmount) >= 10 ); if (visibleInBounds.length > 0) { // Randomly select a location from visible ones with amount >= $10 location = visibleInBounds[Math.floor(Math.random() * visibleInBounds.length)]; } } if (!location) return; if (shouldShowPopup) { showPopup(location, location.coordinates); } const source = map.getSource('dot-point'); if (!source) return; const currentData = source._data; if (!Array.isArray(currentData.features)) return; // Find the existing feature and update its properties const existingFeature = currentData.features.find(feature => feature.geometry.coordinates[0] === location.coordinates[0] && feature.geometry.coordinates[1] === location.coordinates[1] ); if (existingFeature) { existingFeature.properties.isNew = true; source.setData(currentData); setTimeout(() => { existingFeature.properties.isNew = false; source.setData(currentData); }, 1000); } } let index = 0; const maxDots = 1000; let visibleLocations = []; let addedLocations = new Set(); let lastAddedIndex = -1; let initialLocations = new Set(); function addInitialDots() { const source = map.getSource('dot-point'); if (!source) return; const currentData = source._data; if (!Array.isArray(currentData.features)) return; // Sort locations from US westward const sortedLocations = sortLocationsWestward(processedData.slice(0, Math.min(maxDots, processedData.length))); animateDotsAddition(sortedLocations, currentData, source); } function sortLocationsWestward(locations) { const usLongitude = -98.5795; // Approximate center longitude of the US return locations.sort((a, b) => { const aDistance = Math.abs(a.coordinates[0] - usLongitude); const bDistance = Math.abs(b.coordinates[0] - usLongitude); if (aDistance === bDistance) { // If distances are equal, prioritize locations in the Western Hemisphere return b.coordinates[0] - a.coordinates[0]; } return aDistance - bDistance; }); } function animateDotsAddition(locations, currentData, source) { const totalDuration = 1000; // 1 second const intervalDuration = totalDuration / locations.length; let index = 0; function addNextDot() { if (index >= locations.length) return; const location = locations[index]; const locationKey = `${location.coordinates[0]},${location.coordinates[1]}`; if (!addedLocations.has(locationKey)) { addedLocations.add(locationKey); initialLocations.add(locationKey); // Add to initialLocations set const newFeature = { 'type': 'Feature', 'geometry': { 'type': 'Point', 'coordinates': location.coordinates }, 'properties': { 'accountId': location.accountId, 'countryCode': location.countryCode, 'transactionAmount': location.transactionAmount, 'companyType': location.companyType, 'opacity': 1, 'timestamp': Date.now(), 'isNew': false } }; currentData.features.push(newFeature); source.setData(currentData); } index++; setTimeout(addNextDot, intervalDuration); } addNextDot(); } function updateVisibleLocations() { const bounds = map.getBounds(); const edgeThreshold = 40; const centerLongitude = map.getCenter().lng; const currentHemisphere = centerLongitude >= 0 ? 'E' : 'W'; const asianCountries = [ 'AU', 'AF', 'AM', 'AZ', 'BH', 'BD', 'BT', 'BN', 'KH', 'CN', 'CY', 'GE', 'IN', 'ID', 'IR', 'IQ', 'IL', 'JP', 'JO', 'KZ', 'KR', 'KW', 'KG', 'LA', 'LB', 'MY', 'MV', 'MN', 'MM', 'NP', 'OM', 'PK', 'PS', 'PH', 'QA', 'SA', 'SG', 'SY', 'TJ', 'TM', 'TR', 'UZ', 'VN', 'YE', 'TL' ]; // Log the number of items in the Asian Countries list // console.log(`Number of Asian countries: ${asianCountries.length}`); visibleLocations = processedData.filter(location => { const locationKey = `${location.coordinates[0]},${location.coordinates[1]}`; if (!initialLocations.has(locationKey)) return false; if (!bounds.contains(location.coordinates)) return false; const locationHemisphere = location.coordinates[0] >= 0 ? 'E' : 'W'; if (locationHemisphere !== currentHemisphere) return false; const [lng, lat] = location.coordinates; return ( Math.abs(lng - bounds.getWest()) > edgeThreshold && Math.abs(bounds.getEast() - lng) > edgeThreshold && Math.abs(bounds.getNorth() - lat) > edgeThreshold && Math.abs(lat - bounds.getSouth()) > edgeThreshold ); }); const asianLocations = visibleLocations.filter(loc => asianCountries.includes(loc.countryCode)); const easternLocations = visibleLocations.filter(loc => loc.coordinates[0] >= 0); const westernLocations = visibleLocations.filter(loc => loc.coordinates[0] < 0); // console.log(`Visible locations - Asian: ${asianLocations.length}, Eastern: ${easternLocations.length}, Western: ${westernLocations.length}`); // If Asian locations are visible, prioritize them if (asianLocations.length > 0) { visibleLocations = asianLocations; } else { shuffleArray(easternLocations); shuffleArray(westernLocations); visibleLocations = [...easternLocations, ...westernLocations]; } addedLocations = new Set(Array.from(addedLocations).filter(locationKey => { const [lng, lat] = locationKey.split(',').map(Number); return bounds.contains([lng, lat]); })); } // Add this new function after the updateVisibleLocations function function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } function getDistance(coord1, coord2) { const R = 6371e3; const lat1 = coord1[1] * Math.PI / 180; const lat2 = coord2[1] * Math.PI / 180; const deltaLat = (coord2[1] - coord1[1]) * Math.PI / 180; const deltaLon = (coord2[0] - coord1[0]) * Math.PI / 180; const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } map.on('load', () => { map.addImage('pulsing-dot', pulsingDot, { pixelRatio: 2 }); map.addImage('new-pulsing-dot', newItemPulsingDot, { pixelRatio: 2 }); map.addSource('dot-point', { 'type': 'geojson', 'data': { 'type': 'FeatureCollection', 'features': [] } }); // Add a background layer for white dots map.addLayer({ 'id': 'layer-with-white-dot', 'type': 'symbol', 'source': 'dot-point', 'layout': { 'icon-image': 'pulsing-dot', 'icon-allow-overlap': true, }, 'filter': ['!', ['get', 'isNew']] }); // Add a foreground layer for green dots map.addLayer({ 'id': 'layer-with-green-dot', 'type': 'symbol', 'source': 'dot-point', 'layout': { 'icon-image': 'new-pulsing-dot', 'icon-allow-overlap': true, }, 'filter': ['==', ['get', 'isNew'], true] }); // Add this line to trigger the initial dot addition after the layers are added fetchSheetData(); updateMapSize(); }); let config = { columns: {}, }; let processedData = []; function initializeConfig(headerRow, configuredColumns) { if (!Array.isArray(headerRow)) { //console.error("Header row is not an array:", headerRow); return; } const headers = headerRow.map((cell) => cell.trim()); Object.keys(configuredColumns).forEach((index) => { if (index >= headers.length) { //console.warn(`Configured column at index ${index} does not exist in the actual data. It will be removed from the configuration.`); delete configuredColumns[index]; } }); if (Object.keys(configuredColumns).length === 0) { headers.forEach((header, index) => { configuredColumns[index] = { display: header, property: header }; }); } } function processData(headerRow, dataRows, configuredColumns) { const headers = headerRow; const indices = { longitude: headers.indexOf("customer_lon"), latitude: headers.indexOf("customer_lat"), accountId: headers.indexOf("account_id"), companyType: headers.indexOf("account_g2_industry"), transactionAmount: headers.indexOf("transaction_amount_usd") }; if (Object.values(indices).some(index => index === -1)) { console.warn("One or more required columns not found"); return []; } const uniqueCoordinates = new Set(); const bounds = map.getBounds(); const regularData = []; const specialAccountData = []; dataRows.forEach(row => { const longitude = parseFloat(row[indices.longitude]); const latitude = parseFloat(row[indices.latitude]); const accountId = row[indices.accountId]; const companyType = row[indices.companyType]; const transactionAmount = parseFloat(row[indices.transactionAmount]); if (!companyType || companyType.trim() === "" || companyType === "N/A" || isNaN(transactionAmount) || transactionAmount < 0.20 || isNaN(longitude) || isNaN(latitude) || row[indices.longitude].trim() === "" || row[indices.latitude].trim() === "" || !bounds.contains([longitude, latitude])) { return; } const coordinatesKey = `${longitude},${latitude}`; if (uniqueCoordinates.has(coordinatesKey)) return; uniqueCoordinates.add(coordinatesKey); const obj = { coordinates: [longitude, latitude], transactionDate: row[0], transactionAmount, accountId, companyType, countryCode: row[9] }; Object.keys(configuredColumns).forEach(index => { obj[configuredColumns[index].property] = row[index]; }); if (accountId === "0013o00002TkmuZAAR") { specialAccountData.push(obj); } else { regularData.push(obj); } }); // Combine regular data and special account data, with special account data at the end return [...regularData, ...specialAccountData].slice(0, 5000); } // Fallback mode flag globalThis.isFallbackMode = false; // Fetch fallback locations from local JSON function fetchFallbackLocations() { fetch('https://cdn.prod.website-files.com/655381e691678a2583615d93/68814392b355849163e9b0e0_partner_locations.txt') .then(res => res.json()) .then(fallbackData => { globalThis.isFallbackMode = true; plotFallbackPoints(fallbackData); }) .catch(err => { console.error('Failed to load fallback locations:', err); }); } // Plot fallback points as white pulsing dots, no popups function plotFallbackPoints(locations) { const features = locations.slice(0, 1000).map(loc => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [loc.longitude, loc.latitude] }, properties: { isNew: false } })); const source = map.getSource('dot-point'); if (source) { source.setData({ type: 'FeatureCollection', features: features }); } } // Retry logic for Google Sheets fetch function fetchSheetDataWithRetry(retries = 5) { const apiKey = 'AIzaSyD1CpbrG7xrKaWyitHJPDy2TG6m7tGMZWo'; const sheetId = '1zNt50iAESUguEyxN7O3SuJhcHHRhlw9TFqImipjfsdE'; const range = 'Extract1!A1:J'; const sheetUrlWithKey = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${range}?key=${apiKey}`; fetch(sheetUrlWithKey) .then(response => { if (!response.ok) throw new Error(); return response.json(); }) .then(data => { if (data.values && data.values.length > 0) { const headerRow = data.values[0]; const dataRows = data.values.slice(1); initializeConfig(headerRow, config.columns); processedData = processData(headerRow, dataRows, config.columns); startAddingLocations(); } else { throw new Error('No data found in the sheet.'); } }) .catch(error => { if (retries > 1) { setTimeout(() => fetchSheetDataWithRetry(retries - 1), 1000); } else { console.warn('Google Sheets fetch failed after 5 attempts, using fallback.'); fetchFallbackLocations(); } }); } // Replace original fetchSheetData call with retry version function fetchSheetData() { fetchSheetDataWithRetry(5); } document.addEventListener("DOMContentLoaded", fetchSheetData); config.columns = { 0: { display: 'transaction_created_date', property: 'transactionDate' }, 3: { display: 'account_id', property: 'accountId' }, 4: { display: 'account_g2_industry', property: 'companyType' }, 9: { display: 'customer_country_iso', property: 'countryCode' }, 2: { display: 'transaction_amount_usd', property: 'transactionAmount' }, }; // Update the resize event listener window.addEventListener('resize', () => { updateMapSize(); }); // Call updateMapSize after map loads map.on('load', () => { // ... existing load event code ... updateMapSize(); // Add this line }); // Update the event listener at the end of the file document.addEventListener("visibilitychange", handleVisibilityChange); function fetchDataSheetData(retries = 5) { const apiKey = 'AIzaSyD1CpbrG7xrKaWyitHJPDy2TG6m7tGMZWo'; const sheetId = '1grNVq39irzXP9667ETnX3dQYSuRa8hi6X9TkxyoYrtg'; // Updated range to include the new cells const range = 'Sheet1!A1:I57'; const sheetUrlWithKey = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${range}?key=${apiKey}`; fetch(sheetUrlWithKey) .then(response => { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } return response.json(); }) .then(data => { // console.log('Fetched data:', data.values); // Process the data here updateGlobeElements(data.values); }) .catch(error => { console.error("Error fetching data from the sheet:", error); if (retries > 1) { console.log(`Retrying... ${retries - 1} attempts left`); setTimeout(() => fetchDataSheetData(retries - 1), 1000); } else { console.warn('Google Sheets fetch failed after 5 attempts, using fallback metrics.'); fetchMetricsFallback().then(metrics => { if (metrics) { updateGlobeElementsWithFallback(metrics); } }); } }); } function updateGlobeElements(data) { // Get current time in EST const estTime = new Date().toLocaleString("en-US", {timeZone: "America/New_York"}); const estDate = new Date(estTime); // Calculate seconds since midnight EST const midnight = new Date(estDate); midnight.setHours(0,0,0,0); const secondsSinceMidnight = Math.floor((estDate - midnight) / 1000); // Calculate initial sales network value const secondsPerSale = parseFloat(data[27][7]); // H28 cell (seconds per sale) const salesPerSecond = 1 / secondsPerSale; // Calculate sales per second const additionalSales = secondsSinceMidnight * salesPerSecond; // Calculate total additional sales const baseSalesNetworkValue = parseFloat(data[25][4].replace('$', '').replace(/,/g, '')); // E26 const salesNetworkValue = baseSalesNetworkValue + additionalSales; const incrementValue = parseFloat(data[27][5].replace('$', '').replace(/,/g, '')); // Update individual partners calculation const secondsPerPartner = parseFloat(data[23][7]); // H24 cell const partnersPerSecond = 1 / secondsPerPartner; const additionalPartners = Math.floor(secondsSinceMidnight * partnersPerSecond); const baseIndividualPartnersValue = parseFloat(data[21][4].replace(/,/g, '')); // E22 const individualPartnersValue = baseIndividualPartnersValue + additionalPartners; const individualPartnersIncrement = parseFloat(data[23][5].replace(/,/g, '')); // F24 const secondsPerCommission = parseFloat(data[47][7]); // H48 cell const commissionsPerSecond = 1 / secondsPerCommission; const additionalCommissions = secondsSinceMidnight * commissionsPerSecond; const baseCommissionsPaidValue = parseFloat(data[45][4].replace('$', '').replace(/,/g, '')); // E46 const commissionsPaidValue = baseCommissionsPaidValue + additionalCommissions; const commissionsPaidIncrement = parseFloat(data[47][5].replace('$', '').replace(/,/g, '')); // Update customer actions calculation const h44Value = parseFloat(data[43][7]); // H44 cell (seconds value) // Calculate how many complete intervals have passed const completedIntervals = Math.floor(secondsSinceMidnight / h44Value); // Calculate the actual value to add based on completed intervals const additionalActions = completedIntervals; const baseCustomerActionsValue = parseFloat(data[41][4].replace(/,/g, '')); // E42 const customerActionsValue = baseCustomerActionsValue + additionalActions; const customerActionsIncrement = parseFloat(data[43][5].replace(/,/g, '')); // New Values const growthInRevenue = parseFloat(data[3][4].replace(/,/g, '')); const revenueFromPartners = parseFloat(data[4][4].replace('$', '').replace(/,/g, '')); const daysToLaunch = parseFloat(data[5][4].replace('$', '').replace(/,/g, '')); const yoyGrowthPartnerAcquisition = parseFloat(data[6][4].replace('$', '').replace(/,/g, '')); // Display new values document.querySelectorAll('.growth-in-revenue').forEach(el => { el.textContent = growthInRevenue.toLocaleString('en-US') + '%'; }); document.querySelectorAll('.revenue-from-partners').forEach(el => { el.textContent = '$' + revenueFromPartners.toLocaleString('en-US') + 'M'; }); document.querySelectorAll('.days-to-launch').forEach(el => { el.textContent = daysToLaunch.toLocaleString('en-US'); }); document.querySelectorAll('.yoy-growth-partner-acquisition').forEach(el => { el.textContent = yoyGrowthPartnerAcquisition.toLocaleString('en-US') + '%'; }); document.querySelectorAll('.active-partners-full').forEach(el => { el.textContent = Math.round(individualPartnersValue).toLocaleString('en-US'); }); document.querySelectorAll('.active-partners-rounded').forEach(el => { el.textContent = Math.floor(individualPartnersValue / 1000) + 'K+'; }); // Initialize all values updateSalesNetwork(salesNetworkValue); updateCustomerActions(customerActionsValue); updateCommissionsPaid(commissionsPaidValue); updateIndividualPartners(individualPartnersValue); // Rest of the animation code remains the same... let currentValue = salesNetworkValue; let currentCustomerActions = customerActionsValue; let currentCommissionsPaid = commissionsPaidValue; let currentIndividualPartners = individualPartnersValue; function animate() { const animationDuration = 100; function step() { currentValue += incrementValue / 100; updateSalesNetwork(currentValue); currentCustomerActions += customerActionsIncrement / 100; updateCustomerActions(currentCustomerActions); currentCommissionsPaid += commissionsPaidIncrement / 100; updateCommissionsPaid(currentCommissionsPaid); currentIndividualPartners += (individualPartnersIncrement / 100); updateIndividualPartners(currentIndividualPartners); requestAnimationFrame(step); } step(); } animate(); updateCurrentDate(); } function updateSalesNetwork(value) { const formattedValue = '$' + Math.round(value).toLocaleString('en-US'); const element = document.getElementById('globe-sales-network'); if (element) { element.textContent = formattedValue; } } // Modify this function to include decimal points function updateCustomerActions(value) { const formattedValue = Math.round(value).toLocaleString('en-US'); const element = document.getElementById('globe-customer-actions'); if (element) { element.textContent = formattedValue; } } // Add this new function to update commissions paid function updateCommissionsPaid(value) { const formattedValue = '$' + Math.round(value).toLocaleString('en-US'); const element = document.getElementById('globe-commissions-paid'); if (element) { element.textContent = formattedValue; } } // Add this new function to update the current date function updateCurrentDate() { const currentDate = new Date(); const formattedDate = currentDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); // Update both date elements if they exist const element1 = document.getElementById('globe-current-date'); if (element1) { element1.textContent = formattedDate; } const element2 = document.getElementById('globe-current-date-2'); if (element2) { element2.textContent = formattedDate; } } // Add new function to update individual partners function updateIndividualPartners(value) { const formattedValue = Math.round(value).toLocaleString('en-US'); const individualPartnersElement = document.getElementById('individualPartners'); if (individualPartnersElement) { individualPartnersElement.textContent = formattedValue; } } // Call the function to fetch the data fetchDataSheetData(); // --- Fallback Logic for Metrics --- function fetchMetricsFallback() { // Use global metrics data (set by partnerstack-metrics.js) console.log('Using global PARTNERSTACK_METRICS data'); return Promise.resolve(window.PARTNERSTACK_METRICS); } function updateGlobeElementsWithFallback(metrics) { // Send GA4 event when fallback is triggered via GTM dataLayer if (typeof window.dataLayer !== 'undefined') { window.dataLayer.push({ 'event': 'Fallback-Globe_click', 'fallback_id': 'fallback-globe' }); } // Create a map for easy lookup of metric values by ID const metricsMap = {}; metrics.forEach(metric => { metricsMap[metric.id] = metric.value; }); // Helper function to get value by ID with fallback to default const getValue = (id, defaultValue = 0) => metricsMap[id] || defaultValue; // Update the same elements that updateGlobeElements would update const growthInRevenue = getValue('growth-in-revenue'); const revenueFromPartners = getValue('revenue-from-partners'); const daysToLaunch = getValue('days-to-launch'); const yoyGrowthPartnerAcquisition = getValue('yoy-growth-partner-acquisition'); const activePartnersFull = getValue('active-partners-full'); const activePartnersRounded = getValue('active-partners-rounded'); // Use the same update logic as the original function - only update if elements exist document.querySelectorAll('.growth-in-revenue').forEach(el => { el.textContent = growthInRevenue.toLocaleString('en-US') + '%'; }); document.querySelectorAll('.revenue-from-partners').forEach(el => { el.textContent = '$' + revenueFromPartners.toLocaleString('en-US') + 'M'; }); document.querySelectorAll('.days-to-launch').forEach(el => { el.textContent = daysToLaunch.toLocaleString('en-US'); }); document.querySelectorAll('.yoy-growth-partner-acquisition').forEach(el => { el.textContent = yoyGrowthPartnerAcquisition.toLocaleString('en-US') + '%'; }); document.querySelectorAll('.active-partners-full').forEach(el => { el.textContent = activePartnersFull.toLocaleString('en-US'); }); document.querySelectorAll('.active-partners-rounded').forEach(el => { el.textContent = Math.floor(activePartnersFull / 1000) + 'K+'; }); // Start animated counting using fallback values as base with same increment rates as live version const baseSalesNetworkValue = getValue('globe-sales-network'); const baseCustomerActionsValue = getValue('globe-customer-actions'); const baseCommissionsValue = getValue('globe-commissions-paid'); const baseIndividualPartnersValue = activePartnersFull; // Use the same increment calculation logic as the live version // These values simulate the Google Sheets cells that drive the animation rates // Much smaller increments to match realistic counting speed const salesNetworkIncrement = 20; // Simulates data[27][5] - sales increment per 100ms const customerActionsIncrement = 8; // Simulates data[43][5] - customer actions increment per 100ms (visible but slow) const commissionsIncrement = 3; // Simulates data[47][5] - commissions increment per 100ms const individualPartnersIncrement = 0.0185; // Simulates data[23][5] - partners increment per 100ms (1 increment every 1.5 minutes) // Initialize animated values updateSalesNetwork(baseSalesNetworkValue); updateCustomerActions(baseCustomerActionsValue); updateCommissionsPaid(baseCommissionsValue); updateIndividualPartners(baseIndividualPartnersValue); // Start animation with fallback values using same logic as live version let currentValue = baseSalesNetworkValue; let currentCustomerActions = baseCustomerActionsValue; let currentCommissionsPaid = baseCommissionsValue; let currentIndividualPartners = baseIndividualPartnersValue; function animateFallbackValues() { const animationDuration = 100; function step() { // Use the same increment logic as the live version (incrementValue / 100) currentValue += salesNetworkIncrement / 100; updateSalesNetwork(currentValue); currentCustomerActions += customerActionsIncrement / 100; updateCustomerActions(currentCustomerActions); currentCommissionsPaid += commissionsIncrement / 100; updateCommissionsPaid(currentCommissionsPaid); currentIndividualPartners += (individualPartnersIncrement / 100); updateIndividualPartners(currentIndividualPartners); requestAnimationFrame(step); } step(); } animateFallbackValues(); updateCurrentDate(); } // --- End Fallback Logic ---