diff --git a/google-chart-loader.js b/google-chart-loader.js
index c61acb2..3500ff5 100644
--- a/google-chart-loader.js
+++ b/google-chart-loader.js
@@ -373,26 +373,7 @@ Polymer({
* @return {!Promise} promise for the created DataTable
*/
dataTable: function(data) {
- return this._corePackage.then(function(viz) {
- if (data == null) {
- return new viz.DataTable();
- } else if (data.getNumberOfRows) {
- // Data is already a DataTable
- return data;
- } else if (data.cols) { // data.rows may also be specified
- // Data is in the form of object DataTable structure
- return new viz.DataTable(data);
- } else if (data.length > 0) {
- // Data is in the form of a two dimensional array.
- return viz.arrayToDataTable(data);
- } else if (data.length === 0) {
- // Chart data was empty.
- // We return null instead of creating an empty DataTable because most
- // (if not all) charts will render a sticky error in this situation.
- return Promise.reject('Data was empty.');
- }
- return Promise.reject('Data format was not recognized.');
- });
+ return dataTable(data);
},
/**
@@ -424,3 +405,56 @@ Polymer({
});
}
});
+
+/**
+ * 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 {!Array|{cols: !Array, rows: (!Array|undefined)}|undefined} data
+ * the data with which we should use to construct the new DataTable object
+ * @return {!Promise} promise for the created DataTable
+ */
+export async function dataTable(data) {
+ // Ensure that `google.visualization` namespace is added to the document.
+ await load();
+ if (data == null) {
+ return new google.visualization.DataTable();
+ } else if (data.getNumberOfRows) {
+ // Data is already a DataTable
+ return /** @type {!google.visualization.DataTable} */ (data);
+ } else if (data.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.length > 0) {
+ // Data is in the form of a two dimensional array.
+ return google.visualization.arrayToDataTable(data);
+ } else if (data.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.');
+}
diff --git a/google-chart.js b/google-chart.js
index fbe980f..6200903 100644
--- a/google-chart.js
+++ b/google-chart.js
@@ -8,10 +8,46 @@ Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at https://polymer.github.io/PATENTS.txt
*/
import '@polymer/iron-ajax/iron-request.js';
-import './google-chart-loader.js';
import { Polymer } from '@polymer/polymer/lib/legacy/polymer-fn.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { dom } from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import { dataTable, load } from './google-chart-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.
+ *
+ * @type {!Object}
+ */
+const CHART_TYPES = {
+ 'area': 'AreaChart',
+ 'bar': 'BarChart',
+ 'md-bar': 'google.charts.Bar',
+ 'bubble': 'BubbleChart',
+ 'calendar': 'Calendar',
+ 'candlestick': 'CandlestickChart',
+ 'column': 'ColumnChart',
+ 'combo': 'ComboChart',
+ '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
@@ -94,7 +130,6 @@ Polymer({
}
-
`,
@@ -147,7 +182,7 @@ Polymer({
type: {
type: String,
value: 'column',
- observer: '_typeChanged'
+ observer: '_typeChanged',
},
/**
@@ -157,6 +192,7 @@ Polymer({
* 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.
*
@@ -164,7 +200,7 @@ Polymer({
*/
events: {
type: Array,
- value: function() { return []; }
+ value: () => [],
},
/**
@@ -254,7 +290,7 @@ Polymer({
*/
data: {
type: String,
- observer: '_dataChanged'
+ observer: '_dataChanged',
},
/**
@@ -269,7 +305,7 @@ Polymer({
*/
view: {
type: Object,
- observer: '_viewChanged'
+ observer: '_viewChanged',
},
/**
@@ -292,7 +328,7 @@ Polymer({
selection: {
type: Array,
notify: true,
- observer: '_setSelection'
+ observer: '_setSelection',
},
/**
@@ -301,94 +337,82 @@ Polymer({
drawn: {
type: Boolean,
readOnly: true,
- value: false
- },
-
- /** @type {?Object} Internal Google Visualization chart object */
- _chart: {
- type: Object,
- value: null,
+ value: false,
},
- /** @type {?google.visualization.DataView} Internal data state */
- _dataView: {
+ /** Internal data displayed on the chart. */
+ _data: {
type: Object,
- value: null,
},
},
observers: [
- '_draw(_chart, _dataView)',
- '_subOptionChanged(options.*)'
+ 'redraw(_data, options.*)',
],
- listeners: {
- 'google-chart-select': '_updateSelection',
- 'google-chart-ready': '_onChartReady'
- },
+ /**
+ * Internal chart object.
+ * @private {!google.visualization.ChartWrapper|null}
+ */
+ _chartWrapper: null,
- /** @type {?Array} Internal selection state */
- _selection: null,
+ /** @override */
+ ready() {
+ createChartWrapper(this.$.chartdiv).then((chartWrapper) => {
+ this._chartWrapper = chartWrapper;
+ this._typeChanged();
+ google.visualization.events.addListener(chartWrapper, 'ready', () => {
+ this._setDrawn(true);
+ });
+ this._propagateEvents(DEFAULT_EVENTS, chartWrapper);
+ });
+ },
/** Reacts to chart type change. */
- _typeChanged: function() {
- // We need to create a new chart and redraw.
- const loader = /** @type {!GoogleChartLoaderElement} */ (this.$.loader);
- loader.create(this.type, this.$.chartdiv)
- .then(function(chart) {
-
- // only add link stylesheet elements if there are none already
- if (!this.$.styles.children.length) {
- this._localizeGlobalStylesheets();
- }
-
- Object.keys(this.events.concat(['select', 'ready'])
- .reduce(function(set, eventName) {
- set[eventName] = true;
- return set;
- }, {}))
- .forEach(function(eventName) {
- loader.fireOnChartEvent(chart, eventName);
- });
- this._setDrawn(false);
- this._chart = chart;
- }.bind(this));
+ _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', () => {
+ const chart = this._chartWrapper.getChart();
+ if (chart !== lastChart) {
+ this._propagateEvents(this.events.filter((eventName) => !DEFAULT_EVENTS.includes(eventName)), chart);
+ }
+ if (!this.$.styles.children.length) {
+ this._localizeGlobalStylesheets();
+ }
+ if (this.selection) {
+ this._setSelection();
+ }
+ });
+ this.redraw();
},
- /** Reacts to `options` subproperty change. */
- _subOptionChanged: function(optionChangeDetails) {
- this.options = optionChangeDetails.base;
- // Debounce to allow for multiple option changes in one redraw
- this.debounce('optionChangeRedraw', () => {
- this.redraw();
- }, 5);
+ /**
+ * Adds listeners to propagate events from the chart.
+ *
+ * @param {!Array} events
+ * @private
+ */
+ _propagateEvents(events, eventTarget) {
+ for (const eventName of events) {
+ google.visualization.events.addListener(eventTarget, eventName, (event) => {
+ this.fire(`google-chart-${eventName}`, {
+ chart: this._chartWrapper.getChart(),
+ data: event,
+ });
+ });
+ }
},
/** Sets the selectiton on the chart. */
- _setSelection: function() {
- // Note: Some charts (e.g. TreeMap) must have a selection.
- if (!this.drawn || !this.selection || this.selection === this._selection) {
- return;
+ _setSelection() {
+ if (this._chartWrapper == null) return;
+ const chart = this._chartWrapper.getChart();
+ if (chart == null) return;
+ if (chart.setSelection) {
+ chart.setSelection(this.selection);
}
-
- if (this._chart.setSelection) {
- this._chart.setSelection(this.selection);
- }
- this._selection = this.selection;
- },
-
- /** Updates current selection. */
- _updateSelection: function() {
- const selection = this._chart.getSelection();
- this._selection = selection;
- this.selection = selection;
- },
-
- /** Reacts to chart ready event. */
- _onChartReady: function() {
- this._setDrawn(true);
- this._selection = null;
- this._setSelection();
},
/**
@@ -396,73 +420,48 @@ Polymer({
*
* Called automatically when data/type/selection attributes change.
* Call manually to handle view updates, page resizes, etc.
- *
- * @method redraw
*/
- redraw: function() {
- if (!this._chart || !this._dataView) { return; }
- this._draw(this._chart, this._dataView);
- },
-
- /**
- * Renders the chart using the provided data.
- * @param {?Object|undefined} chart Internal Google Visualization chart object.
- * @param {?google.visualization.DataView|undefined} data Internal data state
- */
- _draw: function(chart, data) {
- if(chart == null || data == null) {
- return;
- }
- try {
- this._setDrawn(false);
- chart.draw(data, this.options || {});
- } catch(error) {
- this.$.chartdiv.textContent = error;
- }
+ redraw() {
+ if (this._chartWrapper == null || this._data == null) return;
+ this._chartWrapper.setDataTable(this._data);
+ this._chartWrapper.setOptions(this.options || {});
+
+ this._setDrawn(false);
+ this.debounce('draw', () => {
+ this._chartWrapper.draw();
+ }, 5);
},
/**
* Returns the chart serialized as an image URI.
*
- * Call this after the chart is drawn (google-chart-render event).
+ * Call this after the chart is drawn (google-chart-ready event).
*
* @return {?string} Returns image URI.
*/
get imageURI() {
- if (!this._chart) { return null; }
- return this._chart.getImageURI();
+ if (this._chartWrapper == null) return null;
+ const chart = this._chartWrapper.getChart();
+ return chart && chart.getImageURI();
},
- /**
- * Handles changes to the `view` attribute.
- *
- * @param {!google.visualization.DataView|undefined} view The new view value
- */
- _viewChanged: function(view) {
- if (!view) { return; }
- this._dataView = view;
+ /** Handles changes to the `view` attribute. */
+ _viewChanged() {
+ if (!this.view) { return; }
+ this._data = this.view.toDataTable();
},
/** Handles changes to the rows & columns attributes. */
- _rowsOrColumnsChanged: function() {
- var rows = this.rows, cols = this.cols;
- if (!rows || !cols) { return; }
- const loader = /** @type {!GoogleChartLoaderElement} */ (this.$.loader);
- loader.dataTable(undefined)
- .then(function(dataTable) {
- cols.forEach(function(col) {
- dataTable.addColumn(col);
- });
- dataTable.addRows(rows);
- return dataTable;
- }.bind(this))
- .then(loader.dataView.bind(loader))
- .then(function(dataView) {
- this._dataView = dataView;
- }.bind(this))
- .catch(function(reason) {
- this.$.chartdiv.textContent = reason;
- }.bind(this));
+ async _rowsOrColumnsChanged() {
+ const {rows, cols} = this;
+ if (!rows || !cols) return;
+ try {
+ const dt = await dataTable({cols});
+ dt.addRows(rows);
+ this._data = dt;
+ } catch (reason) {
+ this.$.chartdiv.textContent = reason;
+ }
},
/**
@@ -475,7 +474,7 @@ Polymer({
* string|
* undefined} data The new data value
*/
- _dataChanged: function(data) {
+ _dataChanged(data) {
var dataPromise;
if (!data) { return; }
@@ -505,13 +504,9 @@ Polymer({
// Data is all ready to be processed.
dataPromise = Promise.resolve(data);
}
- const loader = /** @type {!GoogleChartLoaderElement} */ (this.$.loader);
- dataPromise
- .then(loader.dataTable.bind(loader))
- .then(loader.dataView.bind(loader))
- .then(function(dataView) {
- this._dataView = dataView;
- }.bind(this));
+ dataPromise.then(dataTable).then((data) => {
+ this._data = data;
+ });
},
/**
@@ -541,3 +536,14 @@ Polymer({
}
}
});
+
+/**
+ * Creates new `ChartWrapper`.
+ * @param {!Element} container Element in which the chart will be drawn
+ * @return {!Promise}
+ */
+async function createChartWrapper(container) {
+ // Ensure that `google.visualization` namespace is added to the document.
+ await load();
+ return new google.visualization.ChartWrapper({'container': container});
+}
diff --git a/test/basic-tests.html b/test/basic-tests.html
index 060852f..55ee18c 100644
--- a/test/basic-tests.html
+++ b/test/basic-tests.html
@@ -97,11 +97,12 @@
});
test('can change deep options', function(done) {
chart.options = {'title': 'Old Title'};
- var spyRedraw = sinon.spy(chart, 'redraw');
+ var spyRedraw;
var expectedTitle = 'New Title';
var initialDraw = true;
chart.addEventListener('google-chart-ready', function() {
if (initialDraw) {
+ spyRedraw = sinon.spy(chart._chartWrapper, 'draw');
initialDraw = false;
chart.set('options.title', 'Debounced Title');
chart.set('options.title', expectedTitle);
@@ -109,7 +110,7 @@
} else {
assert.equal(chart.$$('text').innerHTML, expectedTitle);
assert.isTrue(spyRedraw.calledOnce);
- chart.redraw.restore();
+ chart._chartWrapper.draw.restore();
done();
}
});
@@ -145,7 +146,7 @@
chart.events = ['onmouseover'];
chart.addEventListener('google-chart-ready', function() {
google.visualization.events.trigger(
- chart._chart, 'onmouseover', {'row': 1, 'column': 5});
+ chart._chartWrapper.getChart(), 'onmouseover', {'row': 1, 'column': 5});
});
chart.addEventListener('google-chart-onmouseover', function(e) {
assert.equal(e.detail.data.row, 1);