// 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 ---