Repository: GoogleWebComponents/google-chart
Branch: main
Commit: 1ee7c173ccc6
Files: 14
Total size: 75.4 KB
Directory structure:
gitextract_1qap31zr/
├── .github/
│ └── workflows/
│ └── tests.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── demo/
│ └── index.html
├── google-chart.ts
├── loader.ts
├── test/
│ ├── basic-tests.ts
│ ├── custom-load-test.html
│ ├── custom-load.ts
│ ├── helpers.ts
│ └── polymer-use-test.ts
└── web-test-runner.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests
on: [push, pull_request]
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: node
cache: 'npm'
- run: npm ci
- run: npm test
================================================
FILE: .gitignore
================================================
node_modules
**/*.d.ts*
**/*.js*
!*.config.js
================================================
FILE: .npmignore
================================================
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# google-chart
[Google Charts API](https://developers.google.com/chart/) web components.
See: [Documentation](https://www.webcomponents.org/element/@google-web-components/google-chart)
[](https://www.npmjs.com/package/@google-web-components/google-chart) [](https://www.webcomponents.org/element/@google-web-components/google-chart)
## Usage
### Installation
```sh
npm i @google-web-components/google-chart
```
### In HTML file
```html
```
### In a LitElement
```typescript
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';
import '@google-web-components/google-chart';
@customElement('new-element')
export class NewElement extends LitElement {
render() {
return html`
`;
}
}
```
### In a Polymer 3 element
```javascript
import {PolymerElement, html} from '@polymer/polymer';
import '@google-web-components/google-chart';
class NewElement extends PolymerElement {
static get template() {
return html`
`;
}
}
customElements.define('new-element', NewElement);
```
### More usage examples
See examples in the demo or try this live [JS bin](https://jsbin.com/zitotejimi/edit?html,output).
## Uprading from 3.x
The component has been migrated to LitElement and uses TypeScript now. This migration introduced two breaking changes.
### Removed Polymer-specific `selection-changed` event
The Polymer-specific `selection-changed` event commonly used for 2-way bindings has been removed.
There were previously two events for observing chart selection changes: `google-chart-select` and the Polymer-generated `selection-changed`.
For consistency with other events (e.g. `google-chart-ready`), we keep only `google-chart-select`.
Polymer components using this feature must be updated to explicitly name the selection event ([details](https://polymer-library.polymer-project.org/3.0/docs/devguide/data-binding#two-way-native)).
In the example below, note the addition of `::google-chart-select`.
```diff
-
+
```
LitElement components using the `selection-changed` event must be updated in a similar fashion:
```diff
-
+
```
### Removed `google-chart-loader` component
Its functionality can be imported from the `loader.js` module:
```javascript
import {dataTable, load} from '@google-web-components/google-chart/loader.js';
```
or you may instead choose to use `google.visualization.ChartWrapper` directly ([example](https://developers.google.com/chart/interactive/docs/reference#chartwrapper-class)).
## Contributing
Instructions for running the tests and demo locally:
### Installation
```sh
git clone https://github.com/GoogleWebComponents/google-chart.git
cd google-chart
npm install
```
### Running the demo locally
```sh
npm start
```
The browser will open automatically.
### Running the tests
```sh
npm test
```
================================================
FILE: demo/index.html
================================================
google-chart Demo
A simple google-chart looks like this:
Charts can be resized with CSS, but you'll need to call the redraw method when the size changes.
Here's a basic responsive example using only CSS and JS (You could also use <iron-media-query>):
Here's a chart that changes data every 3 seconds:
Here's a pie chart with an area selection:
Here's a pie chart listening for `onmouseover`:
Here's a chart defined using data, rather than rows and cols:
And one with some pretty complicated styling, where the data is loaded from an external JSON resource using the data attribute:
Website traffic data by country from an external JSON resource where the data is in raw DataTable format.
Chart gallery
Here's an area chart:
Here's a bar chart:
And here is the material bar chart:
Here's a bubble chart:
Here's a candlestick chart:
Here's a column chart:
Here's a combo chart:
Here's a geo chart:
Here's a histogram:
Here's a line chart:
Here's a material line chart:
Here's a organization chart:
Here's a pie chart:
Here's a sankey diagram:
Here's a scatter chart:
Here's a material scatter chart:
Here's a stepped area chart:
Here's a table chart:
Here's a timeline chart:
Here's the same timeline chart that parses dates from rows:
Here's a wordtree:
Here are three gauges:
Here are three gauges with random data that change every three seconds:
Here's a treemap:
Here's a Gantt chart:
Here is a chart using a DataTable as its source:
Here is a chart using a filtered DataView as its source:
DataViews can be altered, but you'll need to call the redraw method afterward.
Here's an image of the line chart:
================================================
FILE: google-chart.ts
================================================
/**
* @license
* Copyright 2014-2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html, css, LitElement} from 'lit';
import {property} from 'lit/decorators.js';
import {createChartWrapper, dataTable, DataTableLike} from './loader.js';
const DEFAULT_EVENTS = ['ready', 'select'];
/**
* Constructor names for supported chart types.
*
* `ChartWrapper` expects a constructor name and assumes `google.visualization`
* as the default namespace.
*/
const CHART_TYPES: Record = {
'area': 'AreaChart',
'bar': 'BarChart',
'md-bar': 'google.charts.Bar',
'bubble': 'BubbleChart',
'calendar': 'Calendar',
'candlestick': 'CandlestickChart',
'column': 'ColumnChart',
'combo': 'ComboChart',
'gantt': 'Gantt',
'gauge': 'Gauge',
'geo': 'GeoChart',
'histogram': 'Histogram',
'line': 'LineChart',
'md-line': 'google.charts.Line',
'org': 'OrgChart',
'pie': 'PieChart',
'sankey': 'Sankey',
'scatter': 'ScatterChart',
'md-scatter': 'google.charts.Scatter',
'stepped-area': 'SteppedAreaChart',
'table': 'Table',
'timeline': 'Timeline',
'treemap': 'TreeMap',
'wordtree': 'WordTree',
};
/**
* `google-chart` encapsulates Google Charts as a web component, allowing you to
* easily visualize data. From simple line charts to complex hierarchical tree
* maps, the chart element provides a number of ready-to-use chart types.
*
* ```html
*
*
* ```
*
* Note: if you're passing JSON as attributes, single quotes are necessary to be
* valid JSON. See
* https://www.polymer-project.org/1.0/docs/devguide/properties#configuring-object-and-array-properties.
*
* Height and width are specified as style attributes:
* ```css
* google-chart {
* height: 300px;
* width: 50em;
* }
* ```
*
* Data can be provided in one of three ways:
*
* - Via the `cols` and `rows` attributes:
* ```
* cols='[{"label":"Mth", "type":"string"},{"label":"Days", "type":"number"}]'
* rows='[["Jan", 31],["Feb", 28],["Mar", 31]]'
* ```
*
* - Via the `data` attribute, passing in the data directly:
* ```
* data='[["Month", "Days"], ["Jan", 31], ["Feb", 28], ["Mar", 31]]'
* ```
*
* - Via the `data` attribute, passing in the URL to a resource containing the
* data, in JSON format:
* ```
* data='http://example.com/chart-data.json'
* ```
*
* - Via the `data` attribute, passing in a Google DataTable object:
* ```
* data='{{dataTable}}'
* ```
*
* - Via the `view` attribute, passing in a Google DataView object:
* ```
* view='{{dataView}}'
* ```
*
* You can display the charts in locales other than "en" by setting the `lang`
* attribute on the `html` tag of your document:
* ```
*
* ```
*
* @demo demo/index.html
*/
export class GoogleChart extends LitElement {
/** @nocollapse */
static override styles = css`
:host {
display: -webkit-flex;
display: -ms-flex;
display: flex;
margin: 0;
padding: 0;
width: 400px;
height: 300px;
}
:host([hidden]) {
display: none;
}
:host([type="gauge"]) {
width: 300px;
height: 300px;
}
#chartdiv {
width: 100%;
}
/* Workaround for slow initial ready event for tables. */
.google-visualization-table-loadtest {
padding-left: 6px;
}
`;
/**
* Fired after a chart type is rendered and ready for interaction.
*
* @event google-chart-ready
* @param {{chart: !Object}} detail The raw chart object.
*/
/**
* Fired when the user makes a selection in the chart.
*
* @event google-chart-select
* @param {{chart: !Object}} detail The raw chart object.
*/
/**
* Type of the chart.
*
* Should be one of:
* - `area`
* - `(md-)bar`
* - `bubble`
* - `calendar`
* - `candlestick`
* - `column`
* - `combo`
* - `gantt`
* - `gauge`
* - `geo`
* - `histogram`
* - `(md-)line`
* - `org`
* - `pie`
* - `sankey`
* - `(md-)scatter`
* - `stepped-area`
* - `table`
* - `timeline`
* - `treemap`
* - `wordtree`
*
* See Google
* Visualization API reference (Chart Gallery) for details.
*/
@property({type: String, reflect: true})
type = 'column';
/**
* Enumerates the chart events that should be fired.
*
* Charts support a variety of events. By default, this element only
* fires on `ready` and `select`. If you would like to be notified of
* other chart events, use this property to list them.
* Events `ready` and `select` are always fired.
*
* Changes to this property are _not_ observed. Events are attached only
* at chart construction time.
*/
@property({type: Array})
events: string[] = [];
/**
* Sets the options for the chart.
*
* Example:
* ```
* {
* title: "Chart title goes here",
* hAxis: {title: "Categories"},
* vAxis: {title: "Values", minValue: 0, maxValue: 2},
* legend: "none"
* }
* ```
* See Google
* Visualization API reference (Chart Gallery) for the options available
* to each chart type.
*
* Setting this property always redraws the chart. If you would like to make
* changes to a sub-property, be sure to reassign the property:
* ```
* const options = googleChart.options;
* options.vAxis.logScale = true;
* googleChart.options = options;
* ```
* (Note: Missing parent properties are not automatically created.)
*/
@property({type: Object, hasChanged: () => true})
options: {}|undefined = undefined;
/**
* Sets the data columns for this object.
*
* When specifying data with `cols` you must also specify `rows`, and
* not specify `data`.
*
* Example:
* [{label: "Categories", type: "string"},
* {label: "Value", type: "number"}]
* See Google
* Visualization API reference (addColumn) for column definition format.
*/
@property({type: Array})
cols: unknown[]|undefined = undefined;
/**
* Sets the data rows for this object.
*
* When specifying data with `rows` you must also specify `cols`, and
* not specify `data`.
*
* Example:
* [["Category 1", 1.0],
* ["Category 2", 1.1]]
* See Google
* Visualization API reference (addRow) for row format.
*/
@property({type: Array})
rows: unknown[][]|undefined = undefined;
/**
* Sets the entire dataset for this object.
* Can be used to provide the data directly, or to provide a URL from
* which to request the data.
*
* The data format can be a two-dimensional array or the DataTable format
* expected by Google Charts.
* See Google
* Visualization API reference (DataTable constructor) for data table
* format details.
*
* When specifying data with `data` you must not specify `cols` or `rows`.
*
* Example:
* ```
* [["Categories", "Value"],
* ["Category 1", 1.0],
* ["Category 2", 1.1]]
* ```
*/
// Note: type: String, because it is parsed manually in the observer.
@property({type: String})
data: DataTableLike|string|undefined = undefined;
/**
* Sets the entire dataset for this object to a Google DataView.
*
* See Google
* Visualization API reference (DataView) for details.
*
* When specifying data with `view` you must not specify `data`, `cols` or
* `rows`.
*/
@property({type: Object})
view: google.visualization.DataView|undefined = undefined;
/**
* Selected datapoint(s) in the chart.
*
* An array of objects, each with a numeric row and/or column property.
* `row` and `column` are the zero-based row or column number of an item
* in the data table to select.
*
* To select a whole column, set row to null;
* to select a whole row, set column to null.
*
* Example:
* ```
* [{row:0,column:1}, {row:1, column:null}]
* ```
*/
@property({type: Array})
selection: google.visualization.ChartSelection[]|undefined = undefined;
/**
* Whether the chart is currently rendered.
* @export
*/
drawn = false;
/**
* Internal data displayed on the chart.
*/
// tslint:disable-next-line:enforce-name-casing
@property({type: Object}) _data: google.visualization.DataTable|
google.visualization.DataView|undefined = undefined;
/**
* Internal chart object.
*/
private chartWrapper: google.visualization.ChartWrapper|null = null;
private redrawTimeoutId: number|undefined = undefined;
protected override render() {
return html`
`;
}
protected override firstUpdated() {
createChartWrapper(this.shadowRoot!.getElementById('chartdiv')!)
.then(chartWrapper => {
this.chartWrapper = chartWrapper;
this.typeChanged();
google.visualization.events.addListener(chartWrapper, 'ready', () => {
this.drawn = true;
if (this.selection) {
this.selectionChanged();
}
});
google.visualization.events.addListener(
chartWrapper, 'select', () => {
this.selection = chartWrapper.getChart()!.getSelection();
});
this.propagateEvents(DEFAULT_EVENTS, chartWrapper);
});
}
protected override updated(changedProperties: Map) {
if (changedProperties.has('type')) this.typeChanged();
if (changedProperties.has('rows') || changedProperties.has('cols')) {
this.rowsOrColumnsChanged();
}
if (changedProperties.has('data')) this.dataChanged();
if (changedProperties.has('view')) this.viewChanged();
if (changedProperties.has('_data') ||
changedProperties.has('options')) this.redraw();
if (changedProperties.has('selection')) this.selectionChanged();
}
/** Reacts to chart type change. */
private typeChanged() {
if (this.chartWrapper == null) return;
this.chartWrapper.setChartType(CHART_TYPES[this.type] || this.type);
const lastChart = this.chartWrapper.getChart();
google.visualization.events.addOneTimeListener(
this.chartWrapper, 'ready', () => {
// Ready event fires after `chartWrapper` is initialized.
const chart = this.chartWrapper!.getChart();
if (chart !== lastChart) {
this.propagateEvents(
this.events.filter(
(eventName) => !DEFAULT_EVENTS.includes(eventName)),
chart);
}
const stylesDiv = this.shadowRoot!.getElementById('styles')!;
if (!stylesDiv.children.length) {
this.localizeGlobalStylesheets(stylesDiv);
}
});
this.redraw();
}
/**
* Adds listeners to propagate events from the chart.
*/
private propagateEvents(events: string[], eventTarget: unknown) {
for (const eventName of events) {
google.visualization.events.addListener(
eventTarget, eventName, (event: unknown) => {
this.dispatchEvent(new CustomEvent(`google-chart-${eventName}`, {
bubbles: true,
composed: true,
detail: {
// Events fire after `chartWrapper` is initialized.
chart: this.chartWrapper!.getChart(),
data: event,
}
}));
});
}
}
/** Sets the selectiton on the chart. */
private selectionChanged() {
if (this.chartWrapper == null) return;
const chart = this.chartWrapper.getChart();
if (chart == null) return;
if (chart.setSelection) {
// Workaround for timeline chart which emits select event on setSelection.
// See issue #256.
if (this.type === 'timeline') {
const oldSelection = JSON.stringify(chart.getSelection());
const newSelection = JSON.stringify(this.selection);
if (newSelection === oldSelection) return;
}
chart.setSelection(this.selection);
}
}
/**
* Redraws the chart.
*
* Called automatically when data/type/selection attributes change.
* Call manually to handle view updates, page resizes, etc.
*/
redraw() {
if (this.chartWrapper == null || this._data == null) return;
// `ChartWrapper` can be initialized with `DataView` instead of `DataTable`.
this.chartWrapper.setDataTable(
this._data as google.visualization.DataTable);
this.chartWrapper.setOptions(this.options || {});
this.drawn = false;
if (this.redrawTimeoutId !== undefined) clearTimeout(this.redrawTimeoutId);
this.redrawTimeoutId = window.setTimeout(() => {
// Drawing happens after `chartWrapper` is initialized.
this.chartWrapper!.draw();
}, 5);
}
/**
* Returns the chart serialized as an image URI.
*
* Call this after the chart is drawn (`google-chart-ready` event).
*/
get imageURI(): string|null {
if (this.chartWrapper == null) return null;
const chart = this.chartWrapper.getChart();
return chart && (chart as google.visualization.ChartBaseRenderable).getImageURI();
}
/** Handles changes to the `view` attribute. */
private viewChanged() {
if (!this.view) return;
this._data = this.view;
}
/** Handles changes to the rows & columns attributes. */
private async rowsOrColumnsChanged() {
const {rows, cols} = this;
if (!rows || !cols) return;
try {
const dt = await dataTable([cols, ...rows]);
this._data = dt;
} catch (reason) {
this.shadowRoot!.getElementById('chartdiv')!.textContent = String(reason);
}
}
/**
* Handles changes to the `data` attribute.
*/
private dataChanged() {
let data = this.data;
let dataPromise;
if (!data) {
return;
}
let isString = false;
// Polymer 2 will not call observer if type:Object is set and fails, so
// we must parse the string ourselves.
try {
// Try to deserialize the value of the `data` property which might be a
// serialized array.
data = JSON.parse(data as string) as DataTableLike;
} catch (e) {
isString = typeof data === 'string' || data instanceof String;
}
if (isString) {
// Load data asynchronously, from external URL.
dataPromise = fetch(data as string).then(response => response.json());
} else {
// Data is all ready to be processed.
dataPromise = Promise.resolve(data);
}
dataPromise.then(dataTable).then(data => {
this._data = data;
});
}
/**
* Queries global document head for Google Charts `link#load-css-*` and clones
* them into the local root's `div#styles` element for shadow dom support.
*/
private localizeGlobalStylesheets(stylesDiv: HTMLElement) {
// Get all Google Charts stylesheets.
const stylesheets = Array.from(document.head.querySelectorAll(
'link[rel="stylesheet"][type="text/css"][id^="load-css-"]'));
for (const stylesheet of stylesheets) {
// Clone necessary stylesheet attributes.
const clonedStylesheet = document.createElement('link');
clonedStylesheet.setAttribute('rel', 'stylesheet');
clonedStylesheet.setAttribute('type', 'text/css');
// `href` is always present.
clonedStylesheet.setAttribute('href', stylesheet.getAttribute('href')!);
stylesDiv.appendChild(clonedStylesheet);
}
}
}
customElements.define('google-chart', GoogleChart);
declare global {
interface HTMLElementTagNameMap {
'google-chart': GoogleChart;
}
}
================================================
FILE: loader.ts
================================================
/**
* @license
* Copyright 2014-2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {trustedResourceUrl} from 'safevalues';
import {safeScriptEl} from 'safevalues/dom';
/**
* Promise that resolves when the gviz loader script is loaded, which
* provides access to the Google Charts loading API.
*/
const loaderPromise: Promise = new Promise((resolve, reject) => {
// Resolve immediately if the loader script has been added already and
// `google.charts.load` is available. Adding the loader script twice throws
// an error.
if (typeof google !== 'undefined' && google.charts &&
typeof google.charts.load === 'function') {
resolve();
} else {
// Try to find existing loader script.
let loaderScript: HTMLScriptElement|null = document.querySelector(
'script[src="https://www.gstatic.com/charts/loader.js"]');
if (!loaderScript) {
// If the loader is not present, add it.
loaderScript = document.createElement('script');
safeScriptEl.setSrc(
loaderScript,
trustedResourceUrl`https://www.gstatic.com/charts/loader.js`);
document.head.appendChild(loaderScript);
}
loaderScript.addEventListener('load', resolve as () => void);
loaderScript.addEventListener('error', reject);
}
});
interface LoadSettings {
version?: string;
packages?: string[];
language?: string;
mapsApiKey?: string;
}
/**
* Loads Google Charts API with the selected settings or using defaults.
*
* The following settings are available:
* - version: which version of library to load, default: 'current',
* - packages: which chart packages to load, default: ['corechart'],
* - language: what language to load library in, default: `lang` attribute on
* `` or 'en' if not specified,
* - mapsApiKey: key to use for maps API.
*/
export async function load(settings: LoadSettings = {}): Promise {
await loaderPromise;
const {
version = 'current',
packages = ['corechart'],
language = document.documentElement.lang || 'en',
mapsApiKey,
} = settings;
return google.charts.load(version, {
'packages': packages,
'language': language,
'mapsApiKey': mapsApiKey,
});
}
/** Types that can be converted to `DataTable`. */
export type DataTableLike = unknown[][]|{cols: unknown[], rows?: unknown[][]}|
google.visualization.DataTable;
/**
* Creates a DataTable object for use with a chart.
*
* Multiple different argument types are supported. This is because the
* result of loading the JSON data URL is fed into this function for
* DataTable construction and its format is unknown.
*
* The data argument can be one of a few options:
*
* - null/undefined: An empty DataTable is created. Columns must be added
* - !DataTable: The object is simply returned
* - {{cols: !Array, rows: !Array}}: A DataTable in object format
* - {{cols: !Array}}: A DataTable in object format without rows
* - !Array: A DataTable in 2D array format
*
* Un-supported types:
*
* - Empty !Array: (e.g. `[]`) While technically a valid data
* format, this is rejected as charts will not render empty DataTables.
* DataTables must at least have columns specified. An empty array is most
* likely due to a bug or bad data. If one wants an empty DataTable, pass
* no arguments.
* - Anything else
*
* See the
* docs for more details.
*
* @param data The data which we should use to construct new DataTable object
*/
export async function dataTable(data: DataTableLike|undefined):
Promise {
// Ensure that `google.visualization` namespace is added to the document.
await load();
if (data == null) {
return new google.visualization.DataTable();
} else if ((data as google.visualization.DataTable).getNumberOfRows!) {
// Data is already a DataTable
return data as google.visualization.DataTable;
} else if ((data as {
cols: unknown[]
}).cols) { // data.rows may also be specified
// Data is in the form of object DataTable structure
return new google.visualization.DataTable(data);
} else if ((data as unknown[][]).length > 0) {
// Data is in the form of a two dimensional array.
return google.visualization.arrayToDataTable(data as unknown[][]);
} else if ((data as unknown[][]).length === 0) {
// Chart data was empty.
// We throw instead of creating an empty DataTable because most
// (if not all) charts will render a sticky error in this situation.
throw new Error('Data was empty.');
}
throw new Error('Data format was not recognized.');
}
/**
* Creates new `ChartWrapper`.
* @param container Element in which the chart will be drawn
*/
export async function createChartWrapper(container: HTMLElement):
Promise {
// Ensure that `google.visualization` namespace is added to the document.
await load();
// Typings suggest that `chartType` is required in `ChartSpecs`, but it works
// without it.
return new google.visualization.ChartWrapper(
{'container': container} as unknown as google.visualization.ChartSpecs);
}
================================================
FILE: test/basic-tests.ts
================================================
/**
* @license
* Copyright 2014-2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {assert} from '@esm-bundle/chai';
import {fixture} from '@open-wc/testing';
import {timeOut} from '@polymer/polymer/lib/utils/async.js';
import {Debouncer} from '@polymer/polymer/lib/utils/debounce.js';
import {spy, SinonSpy} from 'sinon';
import {GoogleChart} from '../google-chart.js';
import {dataTable, load, DataTableLike} from '../loader.js';
import {ready} from './helpers.js';
suite('', function() {
var chart: GoogleChart;
var waitCheckAndDoneDebouncer: Debouncer|null;
setup(async function() {
chart = await fixture('') as GoogleChart;
waitCheckAndDoneDebouncer = null;
});
var waitCheckAndDone = function(check: () => unknown, done: () => void) {
setTimeout(function() {
if (check()) {
waitCheckAndDoneDebouncer = Debouncer.debounce(
waitCheckAndDoneDebouncer, timeOut.after(100), done);
} else {
waitCheckAndDone(check, done);
}
}, 50);
};
suite('Default Functionality', function() {
setup(function() {
chart.data = [ ['Data', 'Value'], ['Something', 1] ];
});
test('fires google-chart-ready event for initial load', function(done) {
chart.addEventListener('google-chart-ready', function() {
assert.isTrue(chart.drawn);
done();
});
});
test('fires google-chart-ready event for redraw call', function(done) {
var drawCount = 0;
chart.addEventListener('google-chart-ready', function() {
assert.isTrue(chart.drawn);
++drawCount;
if (drawCount == 4) { done(); }
else chart.redraw();
});
});
test('default type is column', function() {
assert.equal(chart.type, 'column');
});
test('can change type', function(done) {
chart.type = 'line';
waitCheckAndDone(function() {
// A circle indicates the chart type change was drawn:
return chart.shadowRoot!.querySelector('circle');
}, done);
});
test('default selection is null', function() {
assert.equal(chart.selection, null);
});
test('can change selection', function(done) {
chart.selection = [ {row: 0} ];
waitCheckAndDone(function() {
// A white stroked rectangle signals the selection was drawn:
return chart.shadowRoot!.querySelector('rect[stroke="#ffffff"]');
}, done);
});
test('updates selection', function(done) {
chart.data = [
['Data', 'Value'],
['Something 1', 1],
['Something 2', 2],
['Something 3', 3],
];
chart.addEventListener('google-chart-select', () => {
assert.sameDeepMembers(chart.selection!, [ {row: 2, column: 1} ]);
done();
}, {once: true});
chart.selection = [ {row: 0, column: 1} ];
chart.addEventListener('google-chart-ready', () => {
// Look for something that can be clicked. Find rectangles for legend
// and each bar.
const chartDiv = chart.shadowRoot!.getElementById('chartdiv')!;
const rects = chartDiv.querySelectorAll('rect[fill="#3366cc"]');
// Click on the last bar ('Something 3').
rects[3].dispatchEvent(new MouseEvent('click', {bubbles: true}));
}, {once: true});
});
test('default options are null', function() {
assert.equal(chart.options, null);
});
test('can change options', function(done) {
var expectedTitle = 'New Title';
var initialDraw = true;
chart.addEventListener('google-chart-ready', function() {
if (initialDraw) {
initialDraw = false;
chart.options = {'title': expectedTitle};
} else {
assert.equal(chart.shadowRoot!.querySelector('text')!.innerHTML, expectedTitle);
done();
}
});
});
test('can change deep options', function(done) {
chart.options = {'title': 'Old Title'};
var spyRedraw: SinonSpy;
var expectedTitle = 'New Title';
var initialDraw = true;
chart.addEventListener('google-chart-ready', function() {
if (initialDraw) {
spyRedraw = spy(chart['chartWrapper']!, 'draw');
initialDraw = false;
const options = chart.options as google.visualization.ColumnChartOptions;
options.title = 'Debounced Title';
chart.options = options;
options.title = expectedTitle;
chart.options = options;
assert.isFalse(spyRedraw.called);
} else {
assert.equal(chart.shadowRoot!.querySelector('text')!.innerHTML, expectedTitle);
assert.isTrue(spyRedraw.calledOnce);
spyRedraw.restore();
done();
}
});
});
test('creates png chart uri', function (done) {
chart.addEventListener('google-chart-ready', function(event) {
var uri = chart.imageURI!;
assert.isString(uri);
assert.match(uri, /^data:image\/png;base64/, 'png regexp matches');
done();
});
});
test('can render multiple instances', function (done) {
var secondChart = document.createElement('google-chart');
secondChart.data = [ ['Data', 'Value'], ['Something', 1] ];
// Ensure second chart is rendered. Clean up test.
secondChart.addEventListener('google-chart-ready', function() {
document.body.removeChild(secondChart);
done();
});
document.body.appendChild(secondChart);
});
});
suite('Class', () => {
test('can be created', async () => {
const chart = new GoogleChart();
chart.data = [ ['Data', 'Value'], ['Something', 1] ];
document.body.appendChild(chart);
await ready(chart);
document.body.removeChild(chart);
});
});
suite('Redrawing', () => {
let chart: GoogleChart;
let dt: google.visualization.DataTable;
setup(async () => {
chart = await fixture('') as GoogleChart;
dt = await dataTable(undefined);
dt.addColumn('number', 'x');
dt.addColumn('number', 'y');
dt.addRow([1, 1]);
});
async function countBars(chart: GoogleChart) {
await ready(chart);
return Array.from(chart.shadowRoot!.querySelectorAll('rect[fill="#3366cc"]')).length;
}
test('redraws after DataTable change', async () => {
chart.data = dt;
const barsBefore = await countBars(chart);
dt.addRow([2, 2]);
chart.redraw();
const barsAfter = await countBars(chart);
assert.isAbove(barsAfter, barsBefore);
});
test('redraws after DataView change', async () => {
const view = new google.visualization.DataView(dt);
chart.view = view;
const barsBefore = await countBars(chart);
dt.addRow([2, 2]);
chart.redraw();
const barsAfter = await countBars(chart);
assert.isAbove(barsAfter, barsBefore);
});
});
suite('Events', function() {
setup(function() {
chart.data = [ ['Data', 'Value'], ['Something', 1] ];
});
test('can be added', function(done) {
chart.events = ['onmouseover'];
chart.addEventListener('google-chart-ready', function() {
google.visualization.events.trigger(
chart['chartWrapper']!.getChart(), 'onmouseover', {'row': 1, 'column': 5});
});
chart.addEventListener('google-chart-onmouseover', function(e) {
const {detail} = (e as CustomEvent);
assert.equal(detail.data.row, 1);
assert.equal(detail.data.column, 5);
done();
});
});
});
suite('Data Source Types', function() {
// Waits for the event reporting a successful render. Invalid or missing data sources do not
// trigger the event and will timeout the tests.
function waitForRender(done: () => void) {
chart.addEventListener('google-chart-ready', () => void done());
}
test('[rows] and [cols] without type', function (done) {
chart.cols = ['Data', 'Value'];
chart.rows = [
['Something', 1]
];
waitForRender(done);
});
test('[rows] and [cols] with type', function(done) {
chart.type = 'timeline';
chart.cols = [
'Data',
{type: 'date', label: 'Start'},
{type: 'date', label: 'End'}
];
chart.rows = [[
'Something',
'Date(2024, 6, 1)',
'Date(2024, 7, 1)'
]];
waitForRender(done);
});
var setDataAndWaitForRender = function(data: DataTableLike|string, done: () => void) {
chart.data = data;
waitForRender(done);
};
test('[data] is 2D Array', function(done) {
setDataAndWaitForRender([ ['Data', 'Value'], ['Something', 1] ], done);
});
test('[data] is DataTable Object format', function(done) {
setDataAndWaitForRender({
'cols': [
{'label': 'Data', 'type': 'string'},
{'label': 'Value', 'type': 'number'}
],
'rows': [
{'c': ['Someting', 1]} as any
]
}, done);
});
test('[data] is DataTable', function(done) {
chart.addEventListener('google-chart-ready', function() {
done();
});
dataTable([
['Data', 'Value'],
['Something', 1]
]).then(function(dataTable) {
chart.data = dataTable;
});
});
test('[data] is DataTable from DataSource Query', function(done) {
chart.addEventListener('google-chart-ready', function() {
done();
});
load().then(() => {
const q = new google.visualization.Query('test/query.json');
q.send((res) => {
chart.data = res.getDataTable();
});
});
});
test('[data] is JSON URL for 2D Array', function(done) {
setDataAndWaitForRender('test/test-data-array.json', done);
});
test('[data] is JSON URL for DataTable Object format', function(done) {
setDataAndWaitForRender('test/test-data-object.json', done);
});
test('[view] is DataView', function(done) {
chart.addEventListener('google-chart-ready', function() {
done();
});
dataTable([
['Data', 'Value'],
['Something', 1]
])
.then((dataTable) => {
chart.view = new google.visualization.DataView(dataTable);
});
});
test('multiple calls to JSON URL', function(done) {
setDataAndWaitForRender('test/test-data-array.json', function() {
setDataAndWaitForRender('test/test-data-object.json', done);
});
});
});
suite('Timeline chart workaround', () => {
// Handle `setSelection` on timeline chart that emits `select` event.
test('set selection does not loop', async () => {
chart.type = 'timeline';
chart.cols = [
{type: 'string', label: 'President'},
{type: 'date', label: 'Start'},
{type: 'date', label: 'End'},
];
chart.rows = [
['Washington', new Date(1789, 3, 30), new Date(1797, 2, 4)],
['Adams', new Date(1797, 2, 4), new Date(1801, 2, 4)],
['Jefferson', new Date(1801, 2, 4), new Date(1809, 2, 4)],
];
await ready(chart);
// Set and update the selection.
chart.selection = [{row: 0, column: null}];
chart.selection = [{row: 1, column: null}];
assert.isDefined(chart.selection);
});
});
});
================================================
FILE: test/custom-load-test.html
================================================
================================================
FILE: test/custom-load.ts
================================================
/**
* @license
* Copyright 2014-2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {assert} from '@esm-bundle/chai';
import {fixture} from '@open-wc/testing';
import {runTests} from '@web/test-runner-mocha';
import '../google-chart.js';
import {GoogleChart} from '../google-chart.js';
import {load} from '../loader.js';
import {ready} from './helpers.js';
// This test has to run separately in a clean document because Google Charts
// API is loaded only once per document.
runTests(() => {
suite('Custom load', () => {
suiteSetup(() => {
// Ensure Google Charts is not loaded.
assert.isUndefined(window.google?.visualization?.DataTable);
return load({version: '45.2'});
});
test('loads Google Charts API with custom settings', () => {
// Verify that the library has been loaded with correct settings by
// inspecting scripts added to the document.
assert.isNotNull(document.querySelector('script[src*="charts/45.2"]'));
assert.isNotNull(document.querySelector('script[src*="corechart_module"]'));
assert.isNotNull(document.querySelector('script[src*="__de"]'));
});
test('loads packages for chart type="table"', async () => {
const chart = await fixture('') as GoogleChart;
chart.data = [ ['Data', 'Value'], ['Something', 1] ];
await ready(chart);
const chartDiv = chart.shadowRoot!.getElementById('chartdiv')!;
assert.isAbove(chartDiv.childElementCount, 0);
});
});
});
================================================
FILE: test/helpers.ts
================================================
/**
* @license
* Copyright 2014-2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** Returns a promise that resolves when drawing of the chart is finished. */
export function ready(element: EventTarget): Promise {
return new Promise(resolve => {
element.addEventListener('google-chart-ready', resolve, {once: true});
})
}
================================================
FILE: test/polymer-use-test.ts
================================================
/**
* @license
* Copyright 2014-2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '../google-chart.js';
import {assert} from '@esm-bundle/chai';
import {customElement, property} from '@polymer/decorators';
import {PolymerElement, html} from '@polymer/polymer';
import {GoogleChart} from '../google-chart.js';
import {DataTableLike} from '../loader.js';
import {ready} from './helpers.js';
suite(' use in Polymer element', () => {
let element: GoogleChartTestElement;
setup(async () => {
element = new GoogleChartTestElement();
document.body.append(element);
await ready(element);
});
teardown(() => {
element?.remove();
})
test('passes properties', () => {
const chartDiv =
element.$['chart'].shadowRoot!.getElementById('chartdiv')!;
assert.include(chartDiv.innerText, 'Value');
assert.include(chartDiv.innerText, 'Something');
});
test('deep options change via binding', async () => {
element.set('options.title', 'New title');
const chartDiv =
element.$['chart'].shadowRoot!.getElementById('chartdiv')!;
await ready(element);
assert.include(chartDiv.innerText, 'New title');
});
test('two-way binding', async () => {
// chart-selection-changed fires because the propery has {notify: true}.
const chartSelectionChanged = new Promise(resolve => {
element.addEventListener('chart-selection-changed', resolve, {once: true});
});
// Get chartWrapper and simulate user selection:
// https://developers.google.com/chart/interactive/docs/dev/events#firing-an-event
const chartWrapper: google.visualization.ChartWrapper=
(element.$['chart'] as GoogleChart)['chartWrapper']!;
chartWrapper.getChart()!.setSelection([{row: 1}]);
google.visualization.events.trigger(chartWrapper.getChart(), 'select', {});
await chartSelectionChanged;
assert.sameDeepMembers(element.chartSelection!, [{row: 1, column: null}]);
});
});
@customElement('google-chart-polymer-test')
class GoogleChartTestElement extends PolymerElement {
static get template() {
return html`
`;
}
@property({type: Object})
options: google.visualization.ColumnChartOptions = {};
@property({type: Object})
data: DataTableLike = [
['Data', 'Value'],
['Something', 1],
['Thing', 2],
['Entry', 3],
];
@property({type: Array, notify: true})
chartSelection: unknown[]|undefined;
}
================================================
FILE: web-test-runner.config.js
================================================
// https://modern-web.dev/docs/test-runner/cli-and-configuration/
export default {
files: 'test/**/*test*.(html|js)',
nodeResolve: true,
testFramework: {
// https://mochajs.org/api/mocha
config: {
ui: 'tdd',
timeout: '20000', // default 2000
},
},
};