Google Charts with React Framework¶
Overview¶
Complete guide for integrating Google Charts with React applications, including TypeScript support, component patterns, and best practices for survey data visualization.
Installation & Setup¶
Install Dependencies¶
# Core React dependencies
npm install react react-dom
# TypeScript support (optional but recommended)
npm install -D @types/react @types/react-dom @types/google.visualization
# Alternative: Use react-google-charts wrapper
npm install react-google-charts
Load Google Charts Library¶
// Method 1: Load in index.html
// Add to public/index.html
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
// Method 2: Dynamic loading in React
import { useEffect } from 'react';
const loadGoogleCharts = (): Promise<void> => {
return new Promise((resolve) => {
if (window.google && window.google.charts) {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://www.gstatic.com/charts/loader.js';
script.onload = () => {
window.google.charts.load('current', {
packages: ['corechart', 'gauge', 'geochart', 'sankey', 'treemap', 'timeline', 'calendar', 'table']
});
window.google.charts.setOnLoadCallback(() => resolve());
};
document.head.appendChild(script);
});
};
Base Chart Component¶
Generic Chart Hook¶
// hooks/useGoogleChart.ts
import { useEffect, useRef, useState } from 'react';
interface UseGoogleChartProps {
chartType: string;
data: any[][];
options: any;
packages?: string[];
}
export const useGoogleChart = ({
chartType,
data,
options,
packages = ['corechart']
}: UseGoogleChartProps) => {
const chartRef = useRef<HTMLDivElement>(null);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const loadChart = async () => {
if (!window.google) {
await loadGoogleCharts();
}
if (!window.google.charts) {
window.google.charts.load('current', { packages });
await new Promise(resolve => {
window.google.charts.setOnLoadCallback(resolve);
});
}
setIsLoaded(true);
};
loadChart();
}, [packages]);
useEffect(() => {
if (!isLoaded || !chartRef.current) return;
const chartData = google.visualization.arrayToDataTable(data);
const chart = new google.visualization[chartType](chartRef.current);
chart.draw(chartData, options);
}, [isLoaded, chartType, data, options]);
return chartRef;
};
Chart Components¶
ComboChart Component¶
// components/ComboChart.tsx
import React from 'react';
import { useGoogleChart } from '../hooks/useGoogleChart';
interface ComboChartData {
category: string;
volume: number;
score: number;
}
interface ComboChartProps {
data: ComboChartData[];
title?: string;
width?: number;
height?: number;
volumeLabel?: string;
scoreLabel?: string;
}
export const ComboChart: React.FC<ComboChartProps> = ({
data,
title = "Volume vs Score Analysis",
width = 900,
height = 500,
volumeLabel = "Volume",
scoreLabel = "Score"
}) => {
const chartData = [
['Category', volumeLabel, scoreLabel],
...data.map(item => [item.category, item.volume, item.score])
];
const options = {
title,
width,
height,
vAxes: {
0: {
title: volumeLabel,
textStyle: { color: '#1f77b4' },
titleTextStyle: { color: '#1f77b4' },
format: '#,###'
},
1: {
title: scoreLabel,
textStyle: { color: '#ff7f0e' },
titleTextStyle: { color: '#ff7f0e' },
minValue: 0,
maxValue: 100
}
},
series: {
0: { type: 'columns', targetAxisIndex: 0, color: '#1f77b4' },
1: { type: 'line', targetAxisIndex: 1, color: '#ff7f0e', lineWidth: 3 }
},
legend: { position: 'top', alignment: 'center' },
chartArea: { left: 80, top: 80, width: '75%', height: '70%' }
};
const chartRef = useGoogleChart({
chartType: 'ComboChart',
data: chartData,
options
});
return <div ref={chartRef} className="combo-chart" />;
};
// Usage Example
const App = () => {
const surveyData = [
{ category: "Commercial", volume: 15420, score: 72 },
{ category: "Medicare", volume: 8932, score: 68 },
{ category: "Medicaid", volume: 5621, score: 45 },
{ category: "Individual", volume: 2103, score: 38 }
];
return (
<div>
<h1>Survey Analysis Dashboard</h1>
<ComboChart
data={surveyData}
title="Response Volume vs NPS Score by Segment"
volumeLabel="Response Count"
scoreLabel="NPS Score"
/>
</div>
);
};
Gauge Component¶
// components/GaugeChart.tsx
import React from 'react';
import { useGoogleChart } from '../hooks/useGoogleChart';
interface GaugeChartProps {
value: number;
label: string;
min?: number;
max?: number;
redZone?: [number, number];
yellowZone?: [number, number];
greenZone?: [number, number];
width?: number;
height?: number;
}
export const GaugeChart: React.FC<GaugeChartProps> = ({
value,
label,
min = 0,
max = 100,
redZone = [0, 30],
yellowZone = [30, 70],
greenZone = [70, 100],
width = 400,
height = 300
}) => {
const chartData = [
['Label', 'Value'],
[label, value]
];
const options = {
title: `Current ${label}`,
width,
height,
redFrom: redZone[0],
redTo: redZone[1],
yellowFrom: yellowZone[0],
yellowTo: yellowZone[1],
greenFrom: greenZone[0],
greenTo: greenZone[1],
minorTicks: 5,
min,
max
};
const chartRef = useGoogleChart({
chartType: 'Gauge',
data: chartData,
options,
packages: ['gauge']
});
return <div ref={chartRef} className="gauge-chart" />;
};
// Multi-Gauge Dashboard Component
export const KPIDashboard: React.FC = () => {
const kpis = [
{ label: "NPS", value: 72, redZone: [0, 30], yellowZone: [30, 70], greenZone: [70, 100] },
{ label: "CSAT", value: 8.2, min: 0, max: 10, redZone: [0, 5], yellowZone: [5, 7], greenZone: [7, 10] },
{ label: "Response Rate", value: 85, redZone: [0, 40], yellowZone: [40, 70], greenZone: [70, 100] }
];
return (
<div className="kpi-dashboard" style={{ display: 'flex', gap: '20px' }}>
{kpis.map((kpi, index) => (
<GaugeChart key={index} {...kpi} />
))}
</div>
);
};
GeoChart Component¶
// components/GeoChart.tsx
import React from 'react';
import { useGoogleChart } from '../hooks/useGoogleChart';
interface GeoChartData {
location: string;
value: number;
}
interface GeoChartProps {
data: GeoChartData[];
title?: string;
region?: string;
displayMode?: 'regions' | 'markers' | 'text';
colorAxis?: {
minValue: number;
maxValue: number;
colors: string[];
};
width?: number;
height?: number;
}
export const GeoChart: React.FC<GeoChartProps> = ({
data,
title = "Geographic Data",
region = "US",
displayMode = "regions",
colorAxis = {
minValue: 0,
maxValue: 100,
colors: ['#FF6B6B', '#FFE66D', '#4ECDC4', '#45B7D1']
},
width = 900,
height = 500
}) => {
const chartData = [
['Location', 'Value'],
...data.map(item => [item.location, item.value])
];
const options = {
title,
region,
displayMode,
resolution: region === 'US' ? 'provinces' : 'countries',
width,
height,
colorAxis,
backgroundColor: '#f5f5f5',
datalessRegionColor: '#E8E8E8'
};
const chartRef = useGoogleChart({
chartType: 'GeoChart',
data: chartData,
options,
packages: ['geochart']
});
return <div ref={chartRef} className="geo-chart" />;
};
// Usage with state data
const RegionalAnalysis: React.FC = () => {
const stateData = [
{ location: "US-CA", value: 75 },
{ location: "US-TX", value: 68 },
{ location: "US-NY", value: 72 },
{ location: "US-FL", value: 65 }
];
return (
<GeoChart
data={stateData}
title="NPS Score by State"
region="US"
colorAxis={{
minValue: 50,
maxValue: 80,
colors: ['#e74c3c', '#f39c12', '#f1c40f', '#2ecc71']
}}
/>
);
};
Advanced Patterns¶
Chart with Loading State¶
// components/ChartWithLoading.tsx
import React, { useState, useEffect } from 'react';
import { ComboChart } from './ComboChart';
interface ChartWithLoadingProps {
dataUrl: string;
}
export const ChartWithLoading: React.FC<ChartWithLoadingProps> = ({ dataUrl }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(dataUrl);
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [dataUrl]);
if (loading) {
return (
<div className="chart-loading">
<div className="spinner">Loading chart...</div>
</div>
);
}
if (error) {
return (
<div className="chart-error">
<p>Error loading chart: {error}</p>
</div>
);
}
return <ComboChart data={data} />;
};
Interactive Chart with Events¶
// components/InteractiveChart.tsx
import React, { useCallback } from 'react';
import { useGoogleChart } from '../hooks/useGoogleChart';
export const InteractiveChart: React.FC<{ data: any[] }> = ({ data }) => {
const handleChartSelect = useCallback((chart: any) => {
google.visualization.events.addListener(chart, 'select', () => {
const selection = chart.getSelection();
if (selection.length > 0) {
const row = selection[0].row;
const category = data[row + 1][0]; // +1 because of header
const value = data[row + 1][1];
console.log(`Selected: ${category} - ${value}`);
// Trigger custom event or callback
onSelectionChange?.(category, value);
}
});
}, [data]);
const chartRef = useGoogleChart({
chartType: 'ColumnChart',
data,
options: { title: 'Interactive Chart' },
onChartReady: handleChartSelect
});
return <div ref={chartRef} />;
};
Context Provider for Charts¶
Chart Configuration Context¶
// context/ChartContext.tsx
import React, { createContext, useContext, ReactNode } from 'react';
interface ChartConfig {
theme: 'light' | 'dark';
colorScheme: string[];
defaultWidth: number;
defaultHeight: number;
}
interface ChartContextType {
config: ChartConfig;
updateConfig: (config: Partial<ChartConfig>) => void;
}
const ChartContext = createContext<ChartContextType | undefined>(undefined);
export const ChartProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [config, setConfig] = useState<ChartConfig>({
theme: 'light',
colorScheme: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728'],
defaultWidth: 800,
defaultHeight: 400
});
const updateConfig = (newConfig: Partial<ChartConfig>) => {
setConfig(prev => ({ ...prev, ...newConfig }));
};
return (
<ChartContext.Provider value={{ config, updateConfig }}>
{children}
</ChartContext.Provider>
);
};
export const useChartConfig = () => {
const context = useContext(ChartContext);
if (!context) {
throw new Error('useChartConfig must be used within ChartProvider');
}
return context;
};
Custom Hooks¶
Data Transformation Hook¶
// hooks/useChartData.ts
import { useMemo } from 'react';
import { UniversalChartData } from '../types/chartTypes';
export const useChartData = (universalData: UniversalChartData) => {
return useMemo(() => {
switch (universalData.type) {
case 'combo':
return [
['Category', 'Volume', 'Score'],
...universalData.labels.map((label, index) => [
label,
universalData.datasets[0]?.data[index]?.y || 0,
universalData.datasets[1]?.data[index]?.y || 0
])
];
case 'gauge':
return [
['Label', 'Value'],
['Current', universalData.gauge_value]
];
case 'geo':
return [
['Location', 'Value'],
...universalData.geographic_data.map(item => [
item.location,
item.value
])
];
default:
return [];
}
}, [universalData]);
};
Real-time Updates Hook¶
// hooks/useRealTimeChart.ts
import { useState, useEffect } from 'react';
export const useRealTimeChart = (endpoint: string, interval: number = 30000) => {
const [data, setData] = useState(null);
const [lastUpdate, setLastUpdate] = useState(new Date());
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(endpoint);
const newData = await response.json();
setData(newData);
setLastUpdate(new Date());
} catch (error) {
console.error('Failed to fetch real-time data:', error);
}
};
// Initial fetch
fetchData();
// Set up interval
const intervalId = setInterval(fetchData, interval);
return () => clearInterval(intervalId);
}, [endpoint, interval]);
return { data, lastUpdate };
};
TypeScript Definitions¶
Chart Types¶
// types/chartTypes.ts
export interface ChartDataPoint {
x?: string | number;
y?: number;
value?: number;
label?: string;
category?: string;
}
export interface ChartDataset {
label: string;
data: ChartDataPoint[];
backgroundColor?: string[];
borderColor?: string[];
yAxisId?: string;
xAxisId?: string;
}
export interface UniversalChartData {
type: string;
title?: string;
datasets: ChartDataset[];
labels?: string[];
xAxisLabel?: string;
yAxisLabel?: string;
gauge_value?: number;
gauge_min?: number;
gauge_max?: number;
geographic_data?: Array<{
location: string;
value: number;
}>;
}
// Extend window object for Google Charts
declare global {
interface Window {
google: {
charts: {
load: (version: string, options: any) => void;
setOnLoadCallback: (callback: () => void) => void;
};
visualization: {
arrayToDataTable: (data: any[][]) => any;
DataTable: new () => any;
ComboChart: new (element: HTMLElement) => any;
Gauge: new (element: HTMLElement) => any;
GeoChart: new (element: HTMLElement) => any;
events: {
addListener: (chart: any, event: string, callback: () => void) => void;
};
};
};
}
}
Best Practices¶
Performance Optimization¶
// Memoize chart components
const MemoizedComboChart = React.memo(ComboChart);
// Use callback refs for better performance
const useChartRef = () => {
return useCallback((node: HTMLDivElement | null) => {
if (node) {
// Chart initialization logic
}
}, []);
};
// Debounce data updates
import { useDebouncedCallback } from 'use-debounce';
const debouncedUpdate = useDebouncedCallback(
(newData) => {
updateChart(newData);
},
300
);
Error Boundaries¶
// components/ChartErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ChartErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: any) {
console.error('Chart error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="chart-error">
<h3>Chart Error</h3>
<p>Unable to render chart. Please try refreshing.</p>
<details>
<summary>Error Details</summary>
<pre>{this.state.error?.message}</pre>
</details>
</div>
);
}
return this.props.children;
}
}
Testing¶
Jest Testing¶
// __tests__/ComboChart.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import { ComboChart } from '../components/ComboChart';
// Mock Google Charts
const mockGoogle = {
charts: {
load: jest.fn(),
setOnLoadCallback: jest.fn()
},
visualization: {
arrayToDataTable: jest.fn(),
ComboChart: jest.fn(() => ({
draw: jest.fn()
}))
}
};
(global as any).google = mockGoogle;
describe('ComboChart', () => {
const mockData = [
{ category: "Test", volume: 100, score: 75 }
];
it('renders without crashing', () => {
render(<ComboChart data={mockData} />);
expect(screen.getByRole('generic')).toBeInTheDocument();
});
it('calls Google Charts with correct data', () => {
render(<ComboChart data={mockData} title="Test Chart" />);
expect(mockGoogle.visualization.arrayToDataTable).toHaveBeenCalledWith([
['Category', 'Volume', 'Score'],
['Test', 100, 75]
]);
});
});
This comprehensive React guide provides everything needed to implement Google Charts in React applications with TypeScript support, proper error handling, and performance optimization!