UNPKG

highcharts

Version:
358 lines (357 loc) 11.2 kB
/* * * * (c) 2009-2026 Highsoft AS * * A commercial license may be required depending on use. * See www.highcharts.com/license * * * Authors: * - Sophie Bremer * - Wojciech Chmiel * - Gøran Slettemark * - Dawid Draguła * - Kamil Kubik * * */ 'use strict'; import DataModifier from '../Modifiers/DataModifier.js'; import DataTable from '../DataTable.js'; import { addEvent, fireEvent, merge, pick } from '../../Shared/Utilities.js'; /* * * * Class * * */ /** * Abstract class providing an interface for managing a DataConnector. */ class DataConnector { /** * Adds a connector class to the registry. The connector has to provide the * `DataConnector.options` property and the `DataConnector.load` method to * modify the table. * * @private * * @param {string} key * Registry key of the connector class. * * @param {DataConnectorType} DataConnectorClass * Connector class (aka class constructor) to register. * * @return {boolean} * Returns true, if the registration was successful. False is returned, if * their is already a connector registered with this key. */ static registerType(key, DataConnectorClass) { return (!!key && !DataConnector.types[key] && !!(DataConnector.types[key] = DataConnectorClass)); } /** * Whether the connector is currently polling for new data. */ get polling() { return !!this._polling; } /* * * * Constructor * * */ /** * Constructor for the connector class. * * @param {DataConnectorOptions} [options] * Options to use in the connector. */ constructor(options) { /** * Tables managed by this DataConnector instance. */ this.dataTables = {}; /** * Helper flag for detecting whether the data connector is loaded. * @internal */ this.loaded = false; this.metadata = options.metadata || { columns: {} }; this.options = options; // Create a data table for each defined in the dataTables user options. const dataTables = options?.dataTables; let dataTableIndex = 0; if (options.options) { // eslint-disable-next-line no-console console.error('The `DataConnectorOptions.options` property was removed in Dashboards v4.0.0. Check how to upgrade your connector to use the new options structure here: https://api.highcharts.com/dashboards/#interfaces/Data_DataTableOptions.DataTableOptions'); } if (dataTables && dataTables?.length > 0) { for (let i = 0, iEnd = dataTables.length; i < iEnd; ++i) { const dataTable = dataTables[i]; const key = dataTable?.key; this.dataTables[key ?? dataTableIndex] = new DataTable(dataTable); if (!key) { dataTableIndex++; } } } else { // If user options dataTables is not defined, generate a default // table. this.dataTables[0] = new DataTable({ id: options.id // Required by DataTableCore }); } } /* * * * Methods * * */ /** * Returns a single data table instance based on the provided key. * Otherwise, returns the first data table. * * @param {string} [key] * The data table key. * * @return {DataTable} * The data table instance. */ getTable(key) { if (key) { return this.dataTables[key]; } return Object.values(this.dataTables)[0]; } /** * Method for adding metadata for a single column. * * @param {string} name * The name of the column to be described. * * @param {MetaColumn} columnMeta * The metadata to apply to the column. */ describeColumn(name, columnMeta) { const connector = this; const columns = connector.metadata.columns; columns[name] = merge(columns[name] || {}, columnMeta); } /** * Method for applying columns meta information to the whole DataConnector. * * @param {Record<string, MetaColumn>} columns * Pairs of column names and MetaColumn objects. */ describeColumns(columns) { const connector = this; const columnIds = Object.keys(columns); let columnId; while (typeof (columnId = columnIds.pop()) === 'string') { connector.describeColumn(columnId, columns[columnId]); } } /** * Returns the order of columns. * * @return {string[] | undefined} * Order of columns. */ getColumnOrder() { const connector = this, columns = connector.metadata.columns, names = Object.keys(columns || {}); if (names.length) { return names.sort((a, b) => (pick(columns[a].index, 0) - pick(columns[b].index, 0))); } } /** * Retrieves the columns of the dataTable, * applies column order from meta. * * @return {Highcharts.DataTableColumnCollection} * An object with the properties `columnIds` and `columnValues` */ getSortedColumns() { return this.getTable().getColumns(this.getColumnOrder()); } /** * Sets the index and order of columns. * * @param {Array<string>} columnIds * Order of columns. */ setColumnOrder(columnIds) { const connector = this; for (let i = 0, iEnd = columnIds.length; i < iEnd; ++i) { connector.describeColumn(columnIds[i], { index: i }); } } /** * Updates the connector with new options. * * @param {object} newOptions * The new options to be applied to the connector. * * @param {boolean} [reload=true] * Whether to reload the connector after applying the new options. */ async update(newOptions, reload = true) { this.emit({ type: 'beforeUpdate' }); merge(true, this.options, newOptions); const { options } = this; if ('enablePolling' in newOptions || 'dataRefreshRate' in newOptions) { if ('enablePolling' in options && options.enablePolling) { this.stopPolling(); this.startPolling(('dataRefreshRate' in options && typeof options.dataRefreshRate === 'number') ? Math.max(options.dataRefreshRate, 1) * 1000 : 1000); } else { this.stopPolling(); } } if (reload) { await this.load(); } this.emit({ type: 'afterUpdate' }); } /** * The default load method, which fires the `afterLoad` event * * @return {Promise<DataConnector>} * The loaded connector. * * @emits DataConnector#afterLoad */ load() { this.emit({ type: 'afterLoad' }); return Promise.resolve(this); } /** * Applies the data modifiers to the data tables according to the * connector data tables options. */ async applyTableModifiers() { const tableOptionsArray = this.options?.dataTables; for (const [key, table] of Object.entries(this.dataTables)) { // Take data modifier options from the corresponding data table // options, otherwise take the data modifier options from the // connector options. const dataModifierOptions = tableOptionsArray?.find((dataTable) => dataTable.key === key)?.dataModifier ?? this.options?.dataModifier; const ModifierClass = (dataModifierOptions && DataModifier.types[dataModifierOptions.type]); await table.setModifier(ModifierClass ? new ModifierClass(dataModifierOptions) : void 0); } return this; } /** * Starts polling new data after the specific time span in milliseconds. * * @param {number} refreshTime * Refresh time in milliseconds between polls. */ startPolling(refreshTime = 1000) { const connector = this; // Assign a new abort controller. this.pollingController = new AbortController(); // Clear the polling timeout. window.clearTimeout(connector._polling); connector._polling = window.setTimeout( // eslint-disable-next-line @typescript-eslint/no-misused-promises () => connector .load()['catch']((error) => connector.emit({ type: 'loadError', error })) .then(() => { if (connector._polling) { connector.startPolling(refreshTime); } }), refreshTime); } /** * Stops polling data. Shouldn't be performed if polling is already stopped. */ stopPolling() { const connector = this; if (!connector.polling) { return; } // Abort the existing request. connector?.pollingController?.abort(); // Clear the polling timeout. window.clearTimeout(connector._polling); delete connector._polling; } /** * Emits an event on the connector to all registered callbacks of this * event. * * @param {Event} e * Event object containing additional event information. */ emit(e) { fireEvent(this, e.type, e); } /** * Registers a callback for a specific connector event. * * @param {string} type * Event type. * * @param {Function} callback * Function to register for the connector callback. * * @return {Function} * Function to unregister callback from the connector event. */ on(type, callback) { return addEvent(this, type, callback); } /** * Iterates over the dataTables and initiates the corresponding converters. * Updates the dataTables and assigns the first converter. * * @param {T}[data] * Data specific to the corresponding converter. * * @param {CreateConverterFunction}[createConverter] * Creates a specific converter combining the dataTable options. * * @param {ParseDataFunction<T>}[parseData] * Runs the converter parse method with the specific data type. */ initConverters(data, createConverter, parseData) { let index = 0; for (const [key, table] of Object.entries(this.dataTables)) { // Create a proper converter and parse its data. const converter = createConverter(key); const columns = parseData(converter, data); // Update the dataTable. table.deleteColumns(); table.setColumns(columns); // Assign the first converter. if (index === 0) { this.converter = converter; } index++; } } } /* * * * Static Properties * * */ /** * Registry as a record object with connector names and their class. */ DataConnector.types = {}; /* * * * Default Export * * */ export default DataConnector;