Google Charts with Angular Framework¶
Overview¶
Complete guide for integrating Google Charts with Angular applications, including TypeScript support, services, components, and best practices for survey data visualization.
Installation & Setup¶
Install Dependencies¶
# Core Angular dependencies (if not already installed)
ng new survey-dashboard
cd survey-dashboard
# TypeScript types for Google Charts
npm install -D @types/google.visualization
# Alternative: Use angular-google-charts wrapper
npm install angular-google-charts
Load Google Charts Library¶
// Method 1: Add to angular.json
"scripts": [
"https://www.gstatic.com/charts/loader.js"
]
// Method 2: Add to index.html
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
// Method 3: Dynamic loading service (recommended)
// services/google-charts.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class GoogleChartsService {
private isLoaded = false;
private loadingPromise: Promise<void> | null = null;
async loadGoogleCharts(): Promise<void> {
if (this.isLoaded) {
return Promise.resolve();
}
if (this.loadingPromise) {
return this.loadingPromise;
}
this.loadingPromise = new Promise<void>((resolve, reject) => {
if (typeof google !== 'undefined' && google.charts) {
this.isLoaded = true;
resolve();
return;
}
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://www.gstatic.com/charts/loader.js';
script.onload = () => {
google.charts.load('current', {
packages: ['corechart', 'gauge', 'geochart', 'sankey', 'treemap', 'timeline', 'calendar', 'table']
});
google.charts.setOnLoadCallback(() => {
this.isLoaded = true;
resolve();
});
};
script.onerror = () => reject(new Error('Failed to load Google Charts'));
document.head.appendChild(script);
});
return this.loadingPromise;
}
isGoogleChartsLoaded(): boolean {
return this.isLoaded;
}
}
Base Chart Service¶
Chart Data Service¶
// services/chart-data.service.ts
import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
export interface UniversalChartData {
type: string;
title?: string;
datasets: ChartDataset[];
labels?: string[];
gauge_value?: number;
gauge_min?: number;
gauge_max?: number;
geographic_data?: GeographicData[];
}
export interface ChartDataset {
label: string;
data: DataPoint[];
yAxisId?: string;
}
export interface DataPoint {
x?: string | number;
y?: number;
value?: number;
category?: string;
}
export interface GeographicData {
location: string;
value: number;
}
@Injectable({
providedIn: 'root'
})
export class ChartDataService {
private dataSubject = new BehaviorSubject<UniversalChartData | null>(null);
public data$ = this.dataSubject.asObservable();
constructor(private http: HttpClient) {}
async fetchSurveyData(endpoint: string): Promise<UniversalChartData> {
try {
const data = await this.http.get<UniversalChartData>(endpoint).toPromise();
this.dataSubject.next(data);
return data;
} catch (error) {
console.error('Error fetching survey data:', error);
throw error;
}
}
transformToGoogleChartsFormat(universalData: UniversalChartData): any[][] {
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 [];
}
}
}
Chart Components¶
Base Chart Component¶
// components/base-chart/base-chart.component.ts
import { Component, ElementRef, Input, OnInit, OnDestroy, ViewChild, OnChanges, SimpleChanges } from '@angular/core';
import { GoogleChartsService } from '../../services/google-charts.service';
@Component({
selector: 'app-base-chart',
template: `
<div #chartContainer class="chart-container" [style.width.px]="width" [style.height.px]="height">
<div *ngIf="loading" class="loading-spinner">
<mat-spinner diameter="40"></mat-spinner>
<p>Loading chart...</p>
</div>
<div *ngIf="error" class="error-message">
<mat-icon>error</mat-icon>
<p>{{ error }}</p>
</div>
</div>
`,
styles: [`
.chart-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.loading-spinner, .error-message {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.error-message {
color: #f44336;
}
`]
})
export class BaseChartComponent implements OnInit, OnDestroy, OnChanges {
@ViewChild('chartContainer', { static: true }) chartContainer!: ElementRef;
@Input() chartType!: string;
@Input() data: any[][] = [];
@Input() options: any = {};
@Input() width: number = 800;
@Input() height: number = 400;
@Input() packages: string[] = ['corechart'];
loading = true;
error: string | null = null;
private chart: any;
constructor(private googleChartsService: GoogleChartsService) {}
async ngOnInit() {
await this.initializeChart();
}
ngOnChanges(changes: SimpleChanges) {
if ((changes['data'] || changes['options']) && this.chart) {
this.drawChart();
}
}
ngOnDestroy() {
if (this.chart) {
this.chart.clearChart?.();
}
}
private async initializeChart() {
try {
this.loading = true;
this.error = null;
await this.googleChartsService.loadGoogleCharts();
if (!google.visualization[this.chartType]) {
throw new Error(`Chart type ${this.chartType} not supported`);
}
this.chart = new google.visualization[this.chartType](this.chartContainer.nativeElement);
this.drawChart();
} catch (error) {
this.error = `Failed to initialize chart: ${error}`;
console.error('Chart initialization error:', error);
} finally {
this.loading = false;
}
}
private drawChart() {
if (!this.chart || !this.data.length) return;
try {
const dataTable = google.visualization.arrayToDataTable(this.data);
this.chart.draw(dataTable, this.options);
} catch (error) {
this.error = `Failed to draw chart: ${error}`;
console.error('Chart drawing error:', error);
}
}
public redraw() {
this.drawChart();
}
}
ComboChart Component¶
// components/combo-chart/combo-chart.component.ts
import { Component, Input, OnInit } from '@angular/core';
export interface ComboChartData {
category: string;
volume: number;
score: number;
}
@Component({
selector: 'app-combo-chart',
template: `
<div class="combo-chart-wrapper">
<h3 *ngIf="title" class="chart-title">{{ title }}</h3>
<app-base-chart
chartType="ComboChart"
[data]="chartData"
[options]="chartOptions"
[width]="width"
[height]="height">
</app-base-chart>
</div>
`,
styles: [`
.combo-chart-wrapper {
padding: 16px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
background: white;
}
.chart-title {
text-align: center;
margin-bottom: 16px;
color: #333;
}
`]
})
export class ComboChartComponent implements OnInit {
@Input() data: ComboChartData[] = [];
@Input() title: string = '';
@Input() width: number = 900;
@Input() height: number = 500;
@Input() volumeLabel: string = 'Volume';
@Input() scoreLabel: string = 'Score';
chartData: any[][] = [];
chartOptions: any = {};
ngOnInit() {
this.updateChartData();
this.updateChartOptions();
}
ngOnChanges() {
this.updateChartData();
this.updateChartOptions();
}
private updateChartData() {
this.chartData = [
['Category', this.volumeLabel, this.scoreLabel],
...this.data.map(item => [item.category, item.volume, item.score])
];
}
private updateChartOptions() {
this.chartOptions = {
title: this.title,
width: this.width,
height: this.height,
vAxes: {
0: {
title: this.volumeLabel,
textStyle: { color: '#1f77b4' },
titleTextStyle: { color: '#1f77b4' },
format: '#,###'
},
1: {
title: this.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, pointSize: 8 }
},
legend: { position: 'top', alignment: 'center' },
chartArea: { left: 80, top: 80, width: '75%', height: '70%' }
};
}
}
// combo-chart.component.html (alternative template approach)
/*
<div class="combo-chart-container">
<mat-card>
<mat-card-header>
<mat-card-title>{{ title }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<app-base-chart
chartType="ComboChart"
[data]="chartData"
[options]="chartOptions"
[width]="width"
[height]="height">
</app-base-chart>
</mat-card-content>
</mat-card>
</div>
*/
Gauge Component¶
// components/gauge-chart/gauge-chart.component.ts
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-gauge-chart',
template: `
<div class="gauge-chart-wrapper">
<h4 *ngIf="label" class="gauge-label">{{ label }}</h4>
<app-base-chart
chartType="Gauge"
[data]="chartData"
[options]="chartOptions"
[width]="width"
[height]="height"
[packages]="['gauge']">
</app-base-chart>
<div class="gauge-value">{{ value }}</div>
</div>
`,
styles: [`
.gauge-chart-wrapper {
text-align: center;
padding: 16px;
border-radius: 8px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.gauge-label {
margin-bottom: 8px;
color: #333;
font-weight: 500;
}
.gauge-value {
font-size: 24px;
font-weight: bold;
color: #333;
margin-top: 8px;
}
`]
})
export class GaugeChartComponent implements OnInit {
@Input() value: number = 0;
@Input() label: string = '';
@Input() min: number = 0;
@Input() max: number = 100;
@Input() redZone: [number, number] = [0, 30];
@Input() yellowZone: [number, number] = [30, 70];
@Input() greenZone: [number, number] = [70, 100];
@Input() width: number = 400;
@Input() height: number = 300;
chartData: any[][] = [];
chartOptions: any = {};
ngOnInit() {
this.updateChartData();
this.updateChartOptions();
}
ngOnChanges() {
this.updateChartData();
this.updateChartOptions();
}
private updateChartData() {
this.chartData = [
['Label', 'Value'],
[this.label, this.value]
];
}
private updateChartOptions() {
this.chartOptions = {
width: this.width,
height: this.height,
redFrom: this.redZone[0],
redTo: this.redZone[1],
yellowFrom: this.yellowZone[0],
yellowTo: this.yellowZone[1],
greenFrom: this.greenZone[0],
greenTo: this.greenZone[1],
minorTicks: 5,
min: this.min,
max: this.max
};
}
}
// KPI Dashboard Component
@Component({
selector: 'app-kpi-dashboard',
template: `
<div class="kpi-dashboard">
<h2>Key Performance Indicators</h2>
<div class="kpi-grid">
<app-gauge-chart
*ngFor="let kpi of kpis"
[value]="kpi.value"
[label]="kpi.label"
[min]="kpi.min"
[max]="kpi.max"
[redZone]="kpi.redZone"
[yellowZone]="kpi.yellowZone"
[greenZone]="kpi.greenZone">
</app-gauge-chart>
</div>
</div>
`,
styles: [`
.kpi-dashboard {
padding: 24px;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
margin-top: 24px;
}
`]
})
export class KpiDashboardComponent {
kpis = [
{
label: 'NPS Score',
value: 72,
min: 0,
max: 100,
redZone: [0, 30] as [number, number],
yellowZone: [30, 70] as [number, number],
greenZone: [70, 100] as [number, number]
},
{
label: 'CSAT Score',
value: 8.2,
min: 0,
max: 10,
redZone: [0, 5] as [number, number],
yellowZone: [5, 7] as [number, number],
greenZone: [7, 10] as [number, number]
},
{
label: 'Response Rate',
value: 85,
min: 0,
max: 100,
redZone: [0, 40] as [number, number],
yellowZone: [40, 70] as [number, number],
greenZone: [70, 100] as [number, number]
}
];
}
GeoChart Component¶
// components/geo-chart/geo-chart.component.ts
import { Component, Input, OnInit } from '@angular/core';
export interface GeoChartData {
location: string;
value: number;
}
@Component({
selector: 'app-geo-chart',
template: `
<div class="geo-chart-wrapper">
<h3 *ngIf="title" class="chart-title">{{ title }}</h3>
<app-base-chart
chartType="GeoChart"
[data]="chartData"
[options]="chartOptions"
[width]="width"
[height]="height"
[packages]="['geochart']">
</app-base-chart>
<div class="geo-legend" *ngIf="showLegend">
<div class="legend-item" *ngFor="let item of legendItems">
<div class="legend-color" [style.background-color]="item.color"></div>
<span>{{ item.label }}</span>
</div>
</div>
</div>
`,
styles: [`
.geo-chart-wrapper {
padding: 16px;
border-radius: 8px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.chart-title {
text-align: center;
margin-bottom: 16px;
color: #333;
}
.geo-legend {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 2px;
}
`]
})
export class GeoChartComponent implements OnInit {
@Input() data: GeoChartData[] = [];
@Input() title: string = '';
@Input() region: string = 'US';
@Input() displayMode: 'regions' | 'markers' | 'text' = 'regions';
@Input() width: number = 900;
@Input() height: number = 500;
@Input() showLegend: boolean = true;
@Input() colorAxis = {
minValue: 0,
maxValue: 100,
colors: ['#FF6B6B', '#FFE66D', '#4ECDC4', '#45B7D1']
};
chartData: any[][] = [];
chartOptions: any = {};
legendItems: { color: string; label: string }[] = [];
ngOnInit() {
this.updateChartData();
this.updateChartOptions();
this.updateLegend();
}
ngOnChanges() {
this.updateChartData();
this.updateChartOptions();
this.updateLegend();
}
private updateChartData() {
this.chartData = [
['Location', 'Value'],
...this.data.map(item => [item.location, item.value])
];
}
private updateChartOptions() {
this.chartOptions = {
title: this.title,
region: this.region,
displayMode: this.displayMode,
resolution: this.region === 'US' ? 'provinces' : 'countries',
width: this.width,
height: this.height,
colorAxis: this.colorAxis,
backgroundColor: '#f5f5f5',
datalessRegionColor: '#E8E8E8'
};
}
private updateLegend() {
if (!this.showLegend) return;
this.legendItems = [
{ color: this.colorAxis.colors[0], label: `${this.colorAxis.minValue} - Low` },
{ color: this.colorAxis.colors[1], label: 'Medium-Low' },
{ color: this.colorAxis.colors[2], label: 'Medium-High' },
{ color: this.colorAxis.colors[3], label: `High - ${this.colorAxis.maxValue}` }
];
}
}
Advanced Features¶
Real-time Data Service¶
// services/real-time-data.service.ts
import { Injectable } from '@angular/core';
import { Observable, interval, switchMap } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { WebSocketSubject } from 'rxjs/webSocket';
@Injectable({
providedIn: 'root'
})
export class RealTimeDataService {
private wsSubject?: WebSocketSubject<any>;
constructor(private http: HttpClient) {}
// Polling approach
getPollingData(endpoint: string, intervalMs: number = 30000): Observable<any> {
return interval(intervalMs).pipe(
switchMap(() => this.http.get(endpoint))
);
}
// WebSocket approach
getWebSocketData(wsUrl: string): Observable<any> {
if (!this.wsSubject) {
this.wsSubject = new WebSocketSubject(wsUrl);
}
return this.wsSubject.asObservable();
}
sendWebSocketMessage(message: any) {
if (this.wsSubject) {
this.wsSubject.next(message);
}
}
closeWebSocket() {
if (this.wsSubject) {
this.wsSubject.complete();
this.wsSubject = undefined;
}
}
}
Interactive Chart Directive¶
// directives/chart-interactions.directive.ts
import { Directive, ElementRef, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
@Directive({
selector: '[appChartInteractions]'
})
export class ChartInteractionsDirective implements OnInit, OnDestroy {
@Output() chartSelect = new EventEmitter<any>();
@Output() chartReady = new EventEmitter<any>();
@Output() chartError = new EventEmitter<any>();
private chart: any;
private listeners: any[] = [];
constructor(private el: ElementRef) {}
ngOnInit() {
// Wait for chart to be initialized
setTimeout(() => {
this.setupInteractions();
}, 1000);
}
ngOnDestroy() {
this.removeListeners();
}
private setupInteractions() {
// Find chart instance (implementation depends on your chart setup)
const chartElement = this.el.nativeElement.querySelector('.google-visualization-chart');
if (!chartElement) return;
// Add event listeners
if (google && google.visualization && google.visualization.events) {
this.listeners.push(
google.visualization.events.addListener(this.chart, 'select', () => {
const selection = this.chart.getSelection();
this.chartSelect.emit(selection);
})
);
this.listeners.push(
google.visualization.events.addListener(this.chart, 'ready', () => {
this.chartReady.emit(this.chart);
})
);
this.listeners.push(
google.visualization.events.addListener(this.chart, 'error', (error: any) => {
this.chartError.emit(error);
})
);
}
}
private removeListeners() {
if (google && google.visualization && google.visualization.events) {
this.listeners.forEach(listener => {
google.visualization.events.removeListener(listener);
});
}
this.listeners = [];
}
}
Module Setup¶
Charts Module¶
// modules/charts.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { MatCardModule } from '@angular/material/card';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatIconModule } from '@angular/material/icon';
import { BaseChartComponent } from '../components/base-chart/base-chart.component';
import { ComboChartComponent } from '../components/combo-chart/combo-chart.component';
import { GaugeChartComponent } from '../components/gauge-chart/gauge-chart.component';
import { GeoChartComponent } from '../components/geo-chart/geo-chart.component';
import { KpiDashboardComponent } from '../components/kpi-dashboard/kpi-dashboard.component';
import { ChartInteractionsDirective } from '../directives/chart-interactions.directive';
import { GoogleChartsService } from '../services/google-charts.service';
import { ChartDataService } from '../services/chart-data.service';
import { RealTimeDataService } from '../services/real-time-data.service';
@NgModule({
declarations: [
BaseChartComponent,
ComboChartComponent,
GaugeChartComponent,
GeoChartComponent,
KpiDashboardComponent,
ChartInteractionsDirective
],
imports: [
CommonModule,
HttpClientModule,
MatCardModule,
MatProgressSpinnerModule,
MatIconModule
],
providers: [
GoogleChartsService,
ChartDataService,
RealTimeDataService
],
exports: [
BaseChartComponent,
ComboChartComponent,
GaugeChartComponent,
GeoChartComponent,
KpiDashboardComponent,
ChartInteractionsDirective
]
})
export class ChartsModule { }
Dashboard Implementation¶
Survey Dashboard Component¶
// components/survey-dashboard/survey-dashboard.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { ChartDataService } from '../../services/chart-data.service';
import { RealTimeDataService } from '../../services/real-time-data.service';
@Component({
selector: 'app-survey-dashboard',
template: `
<div class="dashboard-container">
<header class="dashboard-header">
<h1>Survey Analytics Dashboard</h1>
<div class="last-updated">
Last Updated: {{ lastUpdated | date:'medium' }}
</div>
</header>
<div class="dashboard-grid">
<!-- KPI Gauges -->
<div class="kpi-section">
<app-kpi-dashboard></app-kpi-dashboard>
</div>
<!-- Response Volume vs NPS -->
<div class="combo-section">
<app-combo-chart
[data]="comboData"
title="Response Volume vs NPS Score by Segment"
volumeLabel="Response Count"
scoreLabel="NPS Score">
</app-combo-chart>
</div>
<!-- Geographic Analysis -->
<div class="geo-section">
<app-geo-chart
[data]="geoData"
title="Regional Satisfaction Scores"
region="US"
[colorAxis]="geoColorAxis">
</app-geo-chart>
</div>
<!-- Filters and Controls -->
<div class="controls-section">
<mat-card>
<mat-card-header>
<mat-card-title>Filters</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-form-field>
<mat-label>Date Range</mat-label>
<mat-date-range-input [rangePicker]="picker">
<input matStartDate placeholder="Start date">
<input matEndDate placeholder="End date">
</mat-date-range-input>
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
<mat-form-field>
<mat-label>Business Segment</mat-label>
<mat-select [(value)]="selectedSegment" (selectionChange)="onSegmentChange($event)">
<mat-option value="all">All Segments</mat-option>
<mat-option value="commercial">Commercial</mat-option>
<mat-option value="medicare">Medicare</mat-option>
<mat-option value="medicaid">Medicaid</mat-option>
</mat-select>
</mat-form-field>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
`,
styles: [`
.dashboard-container {
padding: 24px;
background: #f5f5f5;
min-height: 100vh;
}
.dashboard-header {
text-align: center;
margin-bottom: 32px;
}
.dashboard-header h1 {
margin: 0;
color: #333;
}
.last-updated {
color: #666;
font-size: 14px;
margin-top: 8px;
}
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto auto;
gap: 24px;
}
.kpi-section {
grid-column: 1 / -1;
}
.combo-section, .geo-section {
grid-column: 1 / -1;
}
.controls-section {
grid-column: 1 / -1;
}
@media (max-width: 768px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
}
`]
})
export class SurveyDashboardComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
lastUpdated = new Date();
selectedSegment = 'all';
comboData = [
{ category: "Commercial", volume: 15420, score: 72 },
{ category: "Medicare", volume: 8932, score: 68 },
{ category: "Medicaid", volume: 5621, score: 45 },
{ category: "Individual", volume: 2103, score: 38 }
];
geoData = [
{ location: "US-CA", value: 75 },
{ location: "US-TX", value: 68 },
{ location: "US-NY", value: 72 },
{ location: "US-FL", value: 65 }
];
geoColorAxis = {
minValue: 50,
maxValue: 80,
colors: ['#e74c3c', '#f39c12', '#f1c40f', '#2ecc71']
};
constructor(
private chartDataService: ChartDataService,
private realTimeDataService: RealTimeDataService
) {}
ngOnInit() {
this.setupRealTimeUpdates();
this.loadInitialData();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
private setupRealTimeUpdates() {
// Poll for updates every 30 seconds
this.realTimeDataService.getPollingData('/api/survey-data', 30000)
.pipe(takeUntil(this.destroy$))
.subscribe(data => {
this.updateChartData(data);
this.lastUpdated = new Date();
});
}
private async loadInitialData() {
try {
const data = await this.chartDataService.fetchSurveyData('/api/survey-data');
this.updateChartData(data);
} catch (error) {
console.error('Failed to load initial data:', error);
}
}
private updateChartData(data: any) {
// Update chart data based on received data
if (data.comboData) {
this.comboData = data.comboData;
}
if (data.geoData) {
this.geoData = data.geoData;
}
}
onSegmentChange(event: any) {
// Filter data based on selected segment
this.loadDataForSegment(event.value);
}
private loadDataForSegment(segment: string) {
// Implementation for filtering data by segment
console.log('Loading data for segment:', segment);
}
}
Testing¶
Unit Testing¶
// components/combo-chart/combo-chart.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComboChartComponent } from './combo-chart.component';
import { GoogleChartsService } from '../../services/google-charts.service';
describe('ComboChartComponent', () => {
let component: ComboChartComponent;
let fixture: ComponentFixture<ComboChartComponent>;
let mockGoogleChartsService: jasmine.SpyObj<GoogleChartsService>;
beforeEach(async () => {
const spy = jasmine.createSpyObj('GoogleChartsService', ['loadGoogleCharts', 'isGoogleChartsLoaded']);
await TestBed.configureTestingModule({
declarations: [ComboChartComponent],
providers: [
{ provide: GoogleChartsService, useValue: spy }
]
}).compileComponents();
mockGoogleChartsService = TestBed.inject(GoogleChartsService) as jasmine.SpyObj<GoogleChartsService>;
});
beforeEach(() => {
fixture = TestBed.createComponent(ComboChartComponent);
component = fixture.componentInstance;
// Mock Google Charts
(window as any).google = {
visualization: {
arrayToDataTable: jasmine.createSpy('arrayToDataTable'),
ComboChart: jasmine.createSpy('ComboChart').and.returnValue({
draw: jasmine.createSpy('draw')
})
}
};
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should transform data correctly', () => {
const testData = [
{ category: 'Test', volume: 100, score: 75 }
];
component.data = testData;
component.ngOnInit();
expect(component.chartData).toEqual([
['Category', 'Volume', 'Score'],
['Test', 100, 75]
]);
});
it('should update chart options when inputs change', () => {
component.title = 'Test Chart';
component.volumeLabel = 'Test Volume';
component.scoreLabel = 'Test Score';
component.ngOnInit();
expect(component.chartOptions.title).toBe('Test Chart');
expect(component.chartOptions.vAxes[0].title).toBe('Test Volume');
expect(component.chartOptions.vAxes[1].title).toBe('Test Score');
});
});
This comprehensive Angular guide provides everything needed to implement Google Charts in Angular applications with proper service architecture, component patterns, and testing strategies!