@@ -696,14 +696,29 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
696696 } , [ cameraDistance , shouldShowGlobe , globeSize ] ) ;
697697
698698
699- // Heatmap-style coloring: log-scaled user counts, normalized with a steeper curve so neighboring
700- // countries with different volumes (e.g. US vs Canada vs Mexico) don't all land in the same band.
699+ const totalUsersInCountries = Object . values ( countryData ) . reduce ( ( acc , curr ) => acc + curr , 0 ) ;
700+ const totalPopulationInCountries = countries . features . reduce ( ( acc , curr ) => acc + curr . properties . POP_EST , 0 ) ;
701+ const oneSided95PercentZScore = 1.645 ;
702+
703+ const getCountryColorValue = ( countryUsers : number , countryPopulation : number ) : number | null => {
704+ if ( countryUsers === 0 ) return null ;
705+
706+ const observedProportion = countryUsers / totalUsersInCountries ;
707+ const standardError = Math . sqrt ( observedProportion * ( 1 - observedProportion ) / totalUsersInCountries ) ;
708+ const proportionLowerBound = Math . max ( 0 , observedProportion - oneSided95PercentZScore * standardError ) ;
709+ const populationProportion = countryPopulation / totalPopulationInCountries ;
710+ const likelihoodRatio = proportionLowerBound / populationProportion ;
711+
712+ return Math . max ( 0 , Math . log ( 100 * likelihoodRatio ) ) ;
713+ } ;
714+
715+ // Heatmap-style coloring: population-normalized user concentration, with a
716+ // confidence lower bound so tiny samples don't make a country look too strong.
701717 const numericColorValues = countries . features
702718 . map ( ( country ) => {
703719 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
704720 const countryUsers = countryData [ country . properties . ISO_A2_EH ] ?? 0 ;
705- if ( countryUsers === 0 ) return null ;
706- return Math . log1p ( countryUsers ) ;
721+ return getCountryColorValue ( countryUsers , country . properties . POP_EST ) ;
707722 } )
708723 . filter ( ( v ) : v is number => v !== null )
709724 . sort ( ( a , b ) => a - b ) ;
@@ -720,9 +735,7 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
720735 const colorValues = new Map ( countries . features . map ( ( country ) => {
721736 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
722737 const countryUsers = countryData [ country . properties . ISO_A2_EH ] ?? 0 ;
723- if ( countryUsers === 0 ) return [ country . properties . ISO_A2_EH , null ] as const ;
724-
725- const colorValue = Math . log1p ( countryUsers ) ;
738+ const colorValue = getCountryColorValue ( countryUsers , country . properties . POP_EST ) ;
726739 return [ country . properties . ISO_A2_EH , colorValue ] as const ;
727740 } ) ) ;
728741 const maxColorValue = spreadMax ;
0 commit comments