Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | 1x 7x 2x 5x 1x 4x 1x 3x 1x 2x 1x 1x 1x 3x 6x 3x 3x 3x 3x 2x 2x 2x 1x 1x 3x 12x 1x 11x 11x 1x 3x 1x 1x 6x 1x 3x 1x 3x 1x 2x 5x 5x 2x 1x 2x 3x 1x 1x 1x 3x 1x 2x 3x 3x 2x 1x 1x | import { __ } from '@wordpress/i18n';
/**
* Pure helpers for the WorldMap component; kept separate so the navigation,
* matching and statistics logic is unit-testable without the map stack.
*/
/**
* Determine the next view to navigate to when a map feature is clicked.
*
* Clicking is disabled for the country database type (free), and clicking
* the country that is already in view does nothing.
*
* @param {Object} feature - The clicked GeoJSON feature.
* @param {Object} currentView - The current view from the geo store.
* @param {string} geoIpDatabaseType - 'city' (Pro) or 'country' (free).
* @return {Object|null} The next view config, or null when the click should not navigate.
*/
export const getNextViewConfig = ( feature, currentView, geoIpDatabaseType ) => {
if ( ! feature || ! feature.properties?.iso_a2 ) {
return null;
}
// Disable click functionality for country database type
if ( 'country' === geoIpDatabaseType ) {
return null;
}
// Don't navigate if we're already in this country view
if (
'country' === currentView.level &&
currentView.id === feature.properties.iso_a2
) {
return null;
}
if ( 'world' === currentView.level ) {
return {
level: 'country', // @todo: change to continent after adding proper continent data
id: feature.properties?.iso_a2, // Expects continent ID from GeoJSON feature
parentId: null,
title: `${feature.properties?.name || feature.id}`
};
}
if ( 'continent' === currentView.level ) {
return {
level: 'country',
id: feature.properties?.iso_a2, // Expects country ID from GeoJSON feature
parentId: currentView.id,
title: `${feature.properties?.name || feature.id}`
};
}
return null;
};
/**
* Create a function that matches GeoJSON features with analytics data for
* the given view level.
*
* @param {string} viewLevel - The current view level ('world', 'continent', 'country').
* @param {string} geoIpDatabaseType - 'city' (Pro) or 'country' (free).
* @return {Function} Matcher receiving (feature, datum).
*/
export const createFeatureMatcher = ( viewLevel, geoIpDatabaseType ) => {
return ( feature, datum ) => {
if ( 'world' === viewLevel ) {
const featureCountryCode = feature.properties?.iso_a2;
const datumCountryCode = datum.country_code;
return Boolean(
featureCountryCode &&
datumCountryCode &&
featureCountryCode === datumCountryCode
);
}
if ( 'country' === viewLevel && 'city' === geoIpDatabaseType ) {
// For city database type, match regions within countries
const featureRegionCode = feature.properties?.iso_3166_2;
const datumRegionCode = datum.country_code + '-' + datum.state_code;
return Boolean(
featureRegionCode &&
datumRegionCode &&
featureRegionCode === datumRegionCode
);
}
return false;
};
};
/**
* Create a function that reads the selected metric from an analytics datum
* as an integer, defaulting to 0.
*
* @param {string} selectedMetric - The metric key.
* @return {Function} Accessor receiving a datum.
*/
export const createMetricValueAccessor = ( selectedMetric ) => {
return ( datum ) => {
if ( ! datum ) {
return 0;
}
const value = parseInt( datum[selectedMetric], 10 );
return isNaN( value ) ? 0 : value;
};
};
/**
* Label accessor showing the feature name from its properties.
*
* @param {Object} feature - The GeoJSON feature.
* @return {string} The display label.
*/
export const featureLabel = ( feature ) => {
return (
feature.properties?.name_en ||
feature.properties?.iso_a2 ||
__( 'Unknown', 'burst-statistics' )
);
};
/**
* The color scheme for the selected metric, defaulting to greens.
*
* @param {string} selectedMetric - The metric key.
* @param {Object} metricOptions - Metric options keyed by metric.
* @return {string} The color scheme name.
*/
export const getColorScheme = ( selectedMetric, metricOptions ) => {
return metricOptions[selectedMetric]?.colorScheme || 'greens';
};
/**
* Whether the map should zoom to the country now in view: only when the
* country's overlay data has finished loading.
*
* @param {Object} args - The zoom context.
* @param {Object} args.currentView - Current view from the geo store.
* @param {Object} args.overlayFeatures - Overlay feature collection.
* @param {boolean} args.isGeoFetching - Geo data fetching state.
* @param {boolean} args.isGeoLoading - Geo data loading state.
* @return {boolean} True when the zoom target should be set.
*/
export const shouldZoomToCountry = ({
currentView,
overlayFeatures,
isGeoFetching,
isGeoLoading
}) => {
return (
'country' === currentView.level &&
Boolean( currentView.id ) &&
! isGeoFetching &&
! isGeoLoading &&
0 < ( overlayFeatures?.features?.length ?? 0 )
);
};
/**
* Find the analytics entry without a state code, if any.
*
* @param {Array} analyticsData - The analytics data rows.
* @return {Object|null} The datum without state_code, or null.
*/
export const findMissingStateDatum = ( analyticsData ) => {
return analyticsData.find( ( datum ) => ! datum.state_code ) ?? null;
};
/**
* Calculate descriptive statistics for the analytics data.
*
* @param {Array} analyticsData - The analytics data rows.
* @param {Function} valueAccessor - Metric accessor for a datum.
* @param {number} total - Precomputed total for the metric.
* @return {Object|null} The statistics, or null without positive values.
*/
export const computeDataStatistics = ( analyticsData, valueAccessor, total ) => {
if ( ! analyticsData || 0 === analyticsData.length ) {
return null;
}
const values = analyticsData
.map( ( d ) => valueAccessor( d ) )
.filter( ( v ) => 0 < v );
if ( 0 === values.length ) {
return null;
}
const sorted = [ ...values ].sort( ( a, b ) => a - b );
const mean = values.reduce( ( sum, val ) => sum + val, 0 ) / values.length;
const median = sorted[Math.floor( sorted.length / 2 )];
return {
count: values.length,
min: Math.min( ...values ),
max: Math.max( ...values ),
mean: Math.round( mean ),
median: Math.round( median ),
total
};
};
/**
* Calculate the [min, max] domain for the color scale.
*
* @param {Array} analyticsData - The analytics data rows.
* @param {Function} valueAccessor - Metric accessor for a datum.
* @return {Array} The [min, max] domain, defaulting to [0, 100].
*/
export const computeColorDomain = ( analyticsData, valueAccessor ) => {
if ( ! analyticsData || 0 === analyticsData.length ) {
return [ 0, 100 ];
}
const values = analyticsData
.map( ( d ) => valueAccessor( d ) )
.filter( ( v ) => 0 < v );
if ( 0 === values.length ) {
return [ 0, 100 ];
}
return [ Math.min( ...values ), Math.max( ...values ) ];
};
|