Skip to content

Commit

Permalink
feat!: always load the google.visualization namespace
Browse files Browse the repository at this point in the history
Loading the plain google charts script alone is basically useless.
Now, the `google.visualization` namespace is always loaded as well.
  • Loading branch information
FERNman committed Apr 8, 2020
1 parent 5de09b4 commit 1a9d892
Show file tree
Hide file tree
Showing 9 changed files with 369 additions and 320 deletions.
16 changes: 2 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,20 +296,8 @@ class MyComponent {
}
```

If you don't need a specific chart package but just want to access the `google.charts` namespace,
you can use the method `ScriptLoaderService.loadGoogleCharts()`.
It will load the script into the current browser session.

```typescript
ngOnInit() {
this.loaderService.loadGoogleCharts().subscribe(() => {
console.log(this.loaderService.isGoogleChartsAvailable()); // true

// You can now use `google.charts`
google.charts.load();
});
}
```
The `loadChartPackages` method can also be called without any parameters. This way, only the default
google charts packages will be loaded. These include the namespaces `google.charts` and `google.visualization`, but no charts.

### Preloading the Google Charts script

Expand Down
2 changes: 1 addition & 1 deletion libs/angular-google-charts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export * from './lib/components/chart-wrapper/chart-wrapper.component';
export * from './lib/helpers/chart.helper';
export * from './lib/models/events.model';
export * from './lib/models/chart-type.model';
export { Column } from './lib/models/chart-base.model';
export * from './lib/components/chart-base/chart-base.component';
export * from './lib/script-loader/script-loader.service';
export * from './lib/google-charts.module';
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import { EventEmitter } from '@angular/core';
import { Directive, EventEmitter, Output } from '@angular/core';
import { Observable } from 'rxjs';

import { ChartErrorEvent, ChartReadyEvent, ChartSelectionChangedEvent } from './events.model';
import { ChartErrorEvent, ChartReadyEvent, ChartSelectionChangedEvent } from '../../models/events.model';

export type Column = string | google.visualization.ColumnSpec;
export type Row = (string | number | Date)[];

export interface ChartBase {
@Directive()
// tslint:disable-next-line: directive-class-suffix
export class ChartBase {
/**
* The chart is ready for external method calls.
*
* Emits *after* the chart was drawn for the first time every time the chart gets redrawn.
*/
ready: EventEmitter<ChartReadyEvent>;
@Output()
public ready: EventEmitter<ChartReadyEvent>;

/**
* Emits when an error occurs when attempting to render the chart.
*/
error: EventEmitter<ChartErrorEvent>;
@Output()
public error: EventEmitter<ChartErrorEvent>;

/**
* Emits when the user clicks a bar or legend.
Expand All @@ -25,15 +30,21 @@ export interface ChartBase {
* in the data table is selected; when a legend is selected,
* the corresponding column in the data table is selected.
*/
select: EventEmitter<ChartSelectionChangedEvent>;
@Output()
public select: EventEmitter<ChartSelectionChangedEvent>;

/**
* The drawn chart or `null`.
*/
chart: google.visualization.ChartBase | null;
public chart: google.visualization.ChartBase | null;

/**
* The underlying chart wrapper or `null`.
*/
chartWrapper: google.visualization.ChartWrapper | null;
public chartWrapper: google.visualization.ChartWrapper | null;

/**
* Emits every time the `ChartWrapper` is recreated.
*/
public wrapperReady$: Observable<google.visualization.ChartWrapper>;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { EMPTY, of } from 'rxjs';
import { of } from 'rxjs';

import { ChartType } from '../../models/chart-type.model';
import { ScriptLoaderService } from '../../script-loader/script-loader.service';
Expand All @@ -10,12 +10,21 @@ import { ChartWrapperComponent } from './chart-wrapper.component';
jest.mock('../../script-loader/script-loader.service');

const chartWrapperMock = {
setChartType: jest.fn(),
setDataTable: jest.fn(),
setOptions: jest.fn(),
setContainerId: jest.fn(),
setDataSourceUrl: jest.fn(),
setQuery: jest.fn(),
setRefreshInterval: jest.fn(),
setView: jest.fn(),
draw: jest.fn(),
getChart: jest.fn()
};

const visualizationMock = {
ChartWrapper: jest.fn(),
arrayToDataTable: jest.fn(),
events: {
addListener: jest.fn(),
removeAllListeners: jest.fn()
Expand All @@ -38,43 +47,44 @@ describe('ChartWrapperComponent', () => {
}).compileComponents();
}));

beforeEach(() => {
const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked<ScriptLoaderService>;
scriptLoaderService.loadChartPackages.mockReturnValue(of(null));
});

beforeEach(() => {
fixture = TestBed.createComponent(ChartWrapperComponent);
component = fixture.componentInstance;
fixture.detectChanges();
// No change detection here, we want to invoke the
// lifecycle methods in the unit tests
});

it('should be created', () => {
expect(component).toBeTruthy();
});

it('should load the required package for the chart', () => {
const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked<ScriptLoaderService>;
scriptLoaderService.loadChartPackages.mockReturnValue(EMPTY);
it('should load google charts', () => {
component.ngOnInit();

const specs = { chartType: ChartType.AreaChart };
changeSpecs(specs);

expect(scriptLoaderService.loadChartPackages).toHaveBeenCalledWith('corechart');
const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked<ScriptLoaderService>;
expect(scriptLoaderService.loadChartPackages).toHaveBeenCalled();
});

it('should draw a chart using the provided specs', () => {
const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked<ScriptLoaderService>;
scriptLoaderService.loadChartPackages.mockReturnValue(of(void 0));

const specs = { chartType: ChartType.AreaChart, dataTable: [] };
changeSpecs(specs);
component.specs = specs;
component.ngOnInit();

expect(visualizationMock.ChartWrapper).toHaveBeenCalledWith(specs);
expect(visualizationMock.ChartWrapper).toHaveBeenCalled();
expect(chartWrapperMock.setChartType).toBeCalledWith(specs.chartType);
expect(chartWrapperMock.setDataTable).toHaveBeenCalledWith(specs.dataTable);
expect(chartWrapperMock.draw).toHaveBeenCalled();
});

it('should redraw the chart if the specs change', () => {
const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked<ScriptLoaderService>;
scriptLoaderService.loadChartPackages.mockReturnValue(of(void 0));

const specs = { chartType: ChartType.AreaChart, dataTable: [] } as google.visualization.ChartSpecs;
changeSpecs(specs);
component.specs = specs;
component.ngOnInit();

const newSpecs = { ...specs, chartType: ChartType.GeoChart };
changeSpecs(newSpecs);
Expand All @@ -83,25 +93,21 @@ describe('ChartWrapperComponent', () => {
});

it("should not redraw the chart if the specs didn't change", () => {
const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked<ScriptLoaderService>;
scriptLoaderService.loadChartPackages.mockReturnValue(of(void 0));

const specs = { chartType: ChartType.AreaChart, dataTable: [] } as google.visualization.ChartSpecs;
changeSpecs(specs);
component.specs = specs;
component.ngOnInit();

component.ngOnChanges({});

expect(chartWrapperMock.draw).toHaveBeenCalledTimes(1);
});

it('should overwrite `container` and `containerId` if given', () => {
const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked<ScriptLoaderService>;
scriptLoaderService.loadChartPackages.mockReturnValue(of(void 0));

it('should ignore `container` and `containerId` if given', () => {
const specs = { chartType: ChartType.AreaChart, containerId: 'test', container: {} } as google.visualization.ChartSpecs;
changeSpecs(specs);
component.specs = specs;
component.ngOnInit();

expect(visualizationMock.ChartWrapper).toHaveBeenCalledWith({ chartType: specs.chartType });
expect(chartWrapperMock.setContainerId).not.toBeCalled();
});

describe('chart', () => {
Expand All @@ -116,11 +122,9 @@ describe('ChartWrapperComponent', () => {
});

it('should return the chart wrapper', () => {
const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked<ScriptLoaderService>;
scriptLoaderService.loadChartPackages.mockReturnValue(of(void 0));

const specs = { chartType: ChartType.AreaChart, dataTable: [] } as google.visualization.ChartSpecs;
changeSpecs(specs);
component.specs = specs;
component.ngOnInit();

const wrapper = component.chartWrapper;
expect(wrapper).toBe(chartWrapperMock);
Expand All @@ -131,18 +135,17 @@ describe('ChartWrapperComponent', () => {
const specs = { chartType: ChartType.AreaChart } as google.visualization.ChartSpecs;

beforeEach(() => {
const service = TestBed.inject(ScriptLoaderService) as jest.Mocked<ScriptLoaderService>;
service.loadChartPackages.mockReturnValueOnce(of(void 0));
component.specs = specs;
});

it('should remove all event handlers before redrawing the chart', () => {
changeSpecs(specs);
component.ngOnInit();

expect(visualizationMock.events.removeAllListeners).toHaveBeenCalled();
});

it('should register chart wrapper event handlers', () => {
changeSpecs(specs);
component.ngOnInit();

expect(visualizationMock.events.addListener).toHaveBeenCalledWith(chartWrapperMock, 'ready', expect.any(Function));
expect(visualizationMock.events.addListener).toHaveBeenCalledWith(chartWrapperMock, 'error', expect.any(Function));
Expand All @@ -169,8 +172,7 @@ describe('ChartWrapperComponent', () => {

const selectSpy = jest.fn();
component.select.subscribe(event => selectSpy(event));

changeSpecs(specs);
component.ngOnInit();

expect(selectSpy).not.toHaveBeenCalled();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { Observable } from 'rxjs';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges
} from '@angular/core';
import { Subject } from 'rxjs';

import { getPackageForChart } from '../../helpers/chart.helper';
import { ChartBase } from '../../models/chart-base.model';
import { ChartType } from '../../models/chart-type.model';
import { ChartErrorEvent, ChartReadyEvent, ChartSelectionChangedEvent } from '../../models/events.model';
import { ScriptLoaderService } from '../../script-loader/script-loader.service';
import { ChartBase } from '../chart-base/chart-base.component';

@Component({
selector: 'chart-wrapper',
Expand All @@ -15,7 +23,7 @@ import { ScriptLoaderService } from '../../script-loader/script-loader.service';
exportAs: 'chartWrapper',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChartWrapperComponent implements ChartBase, OnChanges {
export class ChartWrapperComponent implements ChartBase, OnChanges, OnInit {
/**
* Either a JSON object defining the chart, or a serialized string version of that object.
* The format of this object is shown in the
Expand All @@ -37,8 +45,10 @@ export class ChartWrapperComponent implements ChartBase, OnChanges {
public select = new EventEmitter<ChartSelectionChangedEvent>();

private wrapper: google.visualization.ChartWrapper;
private wrapperReadySubject = new Subject<google.visualization.ChartWrapper>();
private initialized = false;

constructor(private element: ElementRef, private loaderService: ScriptLoaderService) {}
constructor(private element: ElementRef, private scriptLoaderService: ScriptLoaderService) {}

public get chart(): google.visualization.ChartBase | null {
if (!this.wrapper) {
Expand All @@ -48,32 +58,54 @@ export class ChartWrapperComponent implements ChartBase, OnChanges {
return this.wrapper.getChart();
}

public get wrapperReady$() {
return this.wrapperReadySubject.asObservable();
}

public get chartWrapper(): google.visualization.ChartWrapper | null {
return this.wrapper;
}

public ngOnInit() {
// We don't need to load any chart packages, the chart wrapper will handle this else for us
this.scriptLoaderService.loadChartPackages().subscribe(() => {
// Only ever create the wrapper once to allow animations to happen if something changes.
this.wrapper = new google.visualization.ChartWrapper();
this.createChart();

this.initialized = true;
});
}

public ngOnChanges(changes: SimpleChanges) {
if (!this.initialized) {
return;
}

if (changes.specs) {
this.createChart();
}
}

private createChart() {
this.loadNeededPackages().subscribe(() => {
this.createWrapper();
});
}

private loadNeededPackages(): Observable<void> {
return this.loaderService.loadChartPackages(getPackageForChart(this.specs.chartType as ChartType));
}
if (!this.specs) {
// When creating the wrapper with empty specs, the google charts library will show an error
// If we don't do this, a javascript error will be thrown, which is not as visible to the user
this.specs = {} as google.visualization.ChartSpecs;
}

private createWrapper() {
const { container, containerId, ...chartSpecs } = this.specs;
this.wrapper = new google.visualization.ChartWrapper(chartSpecs);
this.wrapper.setChartType(this.specs.chartType);
this.wrapper.setDataTable(this.specs.dataTable as any); // The typing here are not correct, this also accepts plain arrays
this.wrapper.setDataSourceUrl(this.specs.dataSourceUrl);
this.wrapper.setDataSourceUrl(this.specs.dataSourceUrl);
this.wrapper.setQuery(this.specs.query);
this.wrapper.setOptions(this.specs.options);
this.wrapper.setRefreshInterval(this.specs.refreshInterval);
this.wrapper.setView(this.specs.view);

this.registerChartEvents();

this.wrapperReadySubject.next(this.wrapper);
this.wrapper.draw(this.element.nativeElement);
}

Expand Down
Loading

0 comments on commit 1a9d892

Please sign in to comment.