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) [![Published on NPM](https://img.shields.io/npm/v/@google-web-components/google-chart.svg)](https://www.npmjs.com/package/@google-web-components/google-chart) [![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](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:

Selected row: None.

Here's a pie chart listening for `onmouseover`:

Moused over row: None.

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 }, }, };