Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3223319
Charts: Add GeoChart display mode option
dognose24 Jul 7, 2026
ca25b97
Premium Analytics: Add Locations geo mode controls
dognose24 Jul 7, 2026
ba075fa
Charts: Surface GeoChart errors
dognose24 Jul 7, 2026
7bcdb99
Premium Analytics: Fall back unsupported location maps
dognose24 Jul 7, 2026
1338470
Premium Analytics: Fix Locations map fallback data
dognose24 Jul 7, 2026
6088074
Premium Analytics: Clarify Locations city map docs
dognose24 Jul 7, 2026
94eeb93
Merge remote-tracking branch 'origin/trunk' into codex/locations-inli…
dognose24 Jul 8, 2026
42dd20f
Premium Analytics: Fix Locations CI issues
dognose24 Jul 8, 2026
9748afc
Premium Analytics: Clear fallback map errors
dognose24 Jul 8, 2026
0e51bbf
Premium Analytics: Avoid unsupported Taiwan map
dognose24 Jul 8, 2026
0ecf4ab
Charts: Detect GeoChart errors rendered into the container
dognose24 Jul 8, 2026
cc7e73f
Premium Analytics: Fall back Locations maps on any province error
dognose24 Jul 8, 2026
b27dc71
Premium Analytics: Learn unsupported province maps at runtime
dognose24 Jul 8, 2026
a60a0f9
Premium Analytics: Cover Cities mode in Locations stories
dognose24 Jul 8, 2026
358902a
Premium Analytics: Stabilize Locations fallback map state
dognose24 Jul 8, 2026
05b6abd
Charts: Use Charts changelog prefix convention
dognose24 Jul 8, 2026
faf13a7
Charts: Document GeoChart display mode and error reporting
dognose24 Jul 8, 2026
5bf1536
Charts: Use coordinate data for the GeoChart markers story
dognose24 Jul 8, 2026
d983413
Charts: Drop unused GeoChart displayMode prop
dognose24 Jul 8, 2026
e695af8
Charts: Filter GeoChart error observer mutations
dognose24 Jul 8, 2026
085f496
Premium Analytics: Normalize Locations attributes in one place
dognose24 Jul 8, 2026
0f584f5
Premium Analytics: Collapse redundant Locations fallback flag
dognose24 Jul 8, 2026
376cb16
Premium Analytics: Document Locations body header stacking intent
dognose24 Jul 8, 2026
0a1e630
Premium Analytics: Clear late map errors for known unsupported countries
dognose24 Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Charts: Add GeoChart error reporting.
151 changes: 148 additions & 3 deletions projects/js-packages/charts/src/charts/geo-chart/geo-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
*/
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { FC, useContext, useMemo } from 'react';
import { Chart, type GoogleChartPackages } from 'react-google-charts';
import { FC, useContext, useEffect, useMemo, useRef } from 'react';
import { Chart, type GoogleChartPackages, type ReactGoogleChartEvent } from 'react-google-charts';
/**
* Internal dependencies
*/
Expand All @@ -15,7 +15,7 @@ import { sanitizeHtml } from '../../utils/sanitize-html';
import { Center } from '../private/center';
import { withResponsive } from '../private/with-responsive';
import styles from './geo-chart.module.scss';
import { GeoChartProps } from './types';
import type { GeoChartError, GeoChartProps } from './types';

const DEFAULT_FEATURE_FILL_COLOR = '#ffffff';
const DEFAULT_BACKGROUND_COLOR = '#ffffff';
Expand All @@ -25,6 +25,89 @@ const DEFAULT_BACKGROUND_COLOR = '#ffffff';
const GEO_CHART_PACKAGES: GoogleChartPackages[] = [ 'corechart', 'controls', 'geochart' ];

type GoogleChartOptions = Record< string, unknown >;
type GoogleChartErrorPayload = {
id?: unknown;
message?: unknown;
detailedMessage?: unknown;
options?: unknown;
};

// Google Charts renders draw errors as DOM elements injected into the chart
// container: a wrapper `<div id="google-visualization-errors-all-N">` holding
// one `<span id="google-visualization-errors-N">` per error. The span id is the
// error id accepted by `google.visualization.errors.removeError()`.
const GOOGLE_CHARTS_ERROR_ID_PREFIX = 'google-visualization-errors-';
const GOOGLE_CHARTS_ERROR_WRAPPER_INFIX = '-all-';

/**
* Collects Google Charts error elements rendered inside a chart container.
*
* @param container - The chart container element to scan.
* @return Errors found in the container, one per error span.
*/
function collectRenderedGeoChartErrors(
container: HTMLElement
): Required< Pick< GeoChartError, 'id' | 'message' > >[] {
const elements = container.querySelectorAll< HTMLElement >(
`[id^="${ GOOGLE_CHARTS_ERROR_ID_PREFIX }"]`
);

return Array.from( elements )
.filter( element => ! element.id.includes( GOOGLE_CHARTS_ERROR_WRAPPER_INFIX ) )
.map( element => ( {
id: element.id,
message: element.textContent?.trim() ?? '',
} ) )
.filter( error => error.message.length > 0 );
}

/**
* Whether a node added to the chart container is — or contains — a Google
* Charts error element. Also matches text appended into an existing error
* span, in case Google fills the message after inserting the element.
*
* @param node - The added DOM node to inspect.
* @return Whether the node involves a Google Charts error element.
*/
function involvesGeoChartErrorElement( node: Node ): boolean {
if ( node.nodeType === Node.TEXT_NODE ) {
return !! node.parentElement?.id.startsWith( GOOGLE_CHARTS_ERROR_ID_PREFIX );
}

if ( ! ( node instanceof HTMLElement ) ) {
return false;
}

return (
node.id.startsWith( GOOGLE_CHARTS_ERROR_ID_PREFIX ) ||
node.querySelector( `[id^="${ GOOGLE_CHARTS_ERROR_ID_PREFIX }"]` ) !== null
);
}

/**
* Normalizes the raw Google Charts error event into the GeoChart error shape.
*
* @param eventArgs - Error event payload from react-google-charts.
* @return Normalized GeoChart error.
*/
function normalizeGeoChartError( eventArgs: unknown ): GeoChartError {
const payload = Array.isArray( eventArgs ) ? eventArgs[ 0 ] : eventArgs;

if ( ! payload || typeof payload !== 'object' ) {
return {};
}

const { id, message, detailedMessage, options } = payload as GoogleChartErrorPayload;

return {
...( typeof id === 'string' && { id } ),
...( typeof message === 'string' && { message } ),
...( typeof detailedMessage === 'string' && { detailedMessage } ),
...( options &&
typeof options === 'object' &&
! Array.isArray( options ) && { options: options as Record< string, unknown > } ),
};
}

/**
* Renders a geographical chart using Google Charts GeoChart to visualize data.
Expand All @@ -41,6 +124,7 @@ type GoogleChartOptions = Record< string, unknown >;
* @param props.height - Height of the chart in pixels
* @param props.region - Region to display ('world', 'US', or ISO 3166-1 alpha-2 code)
* @param props.resolution - Resolution level ('countries', 'provinces', or 'metros')
* @param props.onError - Optional callback for Google Charts errors
* @param props.className - Additional CSS class name for the chart container
* @param props.renderPlaceholder - Optional render function for the loading placeholder
* @return A React component displaying an interactive map with data visualization
Expand All @@ -52,6 +136,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
height,
region = 'world',
resolution = 'countries',
onError,
renderPlaceholder,
} ) => {
const {
Expand All @@ -61,6 +146,49 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
backgroundColor,
},
} = useGlobalChartsContext();
const containerRef = useRef< HTMLDivElement >( null );
const reportedErrorIdsRef = useRef< Set< string > >( new Set() );

// The ChartWrapper `error` event does not fire for every draw failure —
// notably not when GeoChart's asynchronous map-file load fails (e.g.
// `resolution: 'provinces'` for a country without a provinces map). Those
// errors only surface as DOM elements Google injects into the container, so
// watch the container and report them through the same `onError` callback.
useEffect( () => {
const container = containerRef.current;

if ( ! onError || ! container || typeof MutationObserver === 'undefined' ) {
return undefined;
}

const reportRenderedErrors = () => {
for ( const error of collectRenderedGeoChartErrors( container ) ) {
if ( reportedErrorIdsRef.current.has( error.id ) ) {
continue;
}

reportedErrorIdsRef.current.add( error.id );
onError( error );
}
};

// GeoChart mutates the container heavily while drawing and resizing;
// only rescan when an added node involves a Google error element.
const observer = new MutationObserver( records => {
const hasErrorNodes = records.some( record =>
Array.from( record.addedNodes ).some( involvesGeoChartErrorElement )
);

if ( hasErrorNodes ) {
reportRenderedErrors();
}
} );
observer.observe( container, { childList: true, subtree: true } );
// Report errors already rendered before the observer attached.
reportRenderedErrors();

return () => observer.disconnect();
}, [ onError ] );

// Render loading placeholder
const loadingPlaceholder = (
Expand Down Expand Up @@ -150,8 +278,24 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
]
);

const chartEvents = useMemo< ReactGoogleChartEvent[] | undefined >( () => {
if ( ! onError ) {
return undefined;
}

return [
{
eventName: 'error',
callback: ( { eventArgs } ) => {
onError( normalizeGeoChartError( eventArgs ) );
},
},
];
}, [ onError ] );

return (
<Center
ref={ containerRef }
className={ clsx( 'geo-chart', styles.container, className ) }
data-testid="geo-chart"
style={ { width, height, backgroundColor } }
Expand All @@ -163,6 +307,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
height={ height }
data={ sanitizedData.data }
options={ options }
chartEvents={ chartEvents }
loader={ loadingPlaceholder }
/>
</Center>
Expand Down
2 changes: 1 addition & 1 deletion projects/js-packages/charts/src/charts/geo-chart/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { default as GeoChart, GeoChartUnresponsive } from './geo-chart';
export type { GeoChartProps, GeoRegion, GeoResolution } from './types';
export type { GeoChartProps, GeoRegion, GeoResolution, GeoChartError } from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Main component for rendering geographical data on an interactive world map using
| `data` | `GeoData` | - | **Required.** Data in Google Charts format. First row contains column headers, subsequent rows contain data. Countries can be identified by full name (e.g., 'United States') or ISO codes (e.g., 'US'). Full names are recommended for better readability in tooltips. |
| `width` | `number` | - | **Required.** Width of the chart in pixels |
| `height` | `number` | - | **Required.** Height of the chart in pixels |
| `region` | `GeoRegion` | `'world'` | Region to display. Use `'world'` for a global view or any ISO 3166-1 alpha-2 country code (e.g., `'US'`) |
| `resolution` | `GeoResolution` | `'countries'` | Map resolution: `'countries'`, `'provinces'` (state/province level, use with a specific region), or `'metros'` (US only) |
| `onError` | `(error: GeoChartError) => void` | - | Callback fired when Google Charts emits a chart error, including draw errors Google renders into the chart container (e.g. a missing map file) |
| `className` | `string` | - | Additional CSS class name for the chart container |
| `chartId` | `string` | - | Custom chart identifier for accessibility |
| `renderPlaceholder` | `() => React.ReactNode` | - | Optional render function for custom loading placeholder. Called while Google Charts is loading |
Expand All @@ -25,6 +28,9 @@ Main component for rendering geographical data on an interactive world map using
interface GeoChartProps
extends Pick<BaseChartProps, 'className' | 'chartId' | 'width' | 'height'> {
data: GeoData;
region?: GeoRegion;
resolution?: GeoResolution;
onError?: (error: GeoChartError) => void;
renderPlaceholder?: () => React.ReactNode;
}
```
Expand Down Expand Up @@ -62,6 +68,19 @@ const data: GeoData = [
];
```

## GeoChartError Type

```typescript
interface GeoChartError {
id?: string; // error id accepted by google.visualization.errors.removeError()
message?: string;
detailedMessage?: string;
options?: Record<string, unknown>;
}
```

Errors reach `onError` from two paths: the Google ChartWrapper `error` event, and draw errors Google renders directly into the chart container without firing that event (observed via a `MutationObserver`). The second path covers async failures such as a missing map file for an unsupported `provinces` region.

## Theme Properties

The following properties can be customized via the theme system:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,17 @@ The simplest geo chart requires data, width, and height. The `data` prop uses Go
- **`maxWidth`**: Maximum width constraint for responsive charts (default: `1200`)
- **`resizeDebounceTime`**: Debounce delay for resize events in ms (default: `300`)

**Map Scope:**
- **`region`**: Region to display — `'world'` (default) or an ISO 3166-1 alpha-2 country code (e.g., `'US'`)
- **`resolution`**: Map resolution — `'countries'` (default), `'provinces'` (use with a specific region), or `'metros'` (US only)

**Styling & Customization:**
- **`className`**: Additional CSS class name for custom styling
- **`renderPlaceholder`**: Custom render function for the loading placeholder

**Error Handling:**
- **`onError`**: Callback fired when Google Charts emits a chart error, including draw errors rendered into the chart container (e.g. a missing map file)

For detailed prop information and type definitions, see the [Geo Chart API Reference](./?path=/docs/js-packages-charts-library-charts-geo-chart-api-reference--docs).

## Advanced Features
Expand Down
Loading
Loading