UNPKG

@highcharts/dashboards

Version:
497 lines (496 loc) 16.7 kB
/* * * * (c) 2009-2026 Highsoft AS * * Integration of this software requires a license. * - For commercial use, see www.highcharts.com/license * - For non-commercial, see www.highcharts.com/license-eula * * * Authors: * - Gøran Slettemark * - Wojciech Chmiel * - Sebastian Bochan * - Sophie Bremer * - Dawid Draguła * * */ 'use strict'; import Component from '../Component.js'; import DataConverter from '../../../Data/Converters/DataConverter.js'; import Globals from '../../Globals.js'; import HighchartsSyncs from './HighchartsSyncs/HighchartsSyncs.js'; import HighchartsComponentDefaults from './HighchartsComponentDefaults.js'; import DataConverterUtils from '../../../Data/Converters/DataConverterUtils.js'; import DU from '../../Utilities.js'; import { createElement, diffObjects, isString, merge, splat } from '../../../Shared/Utilities.js'; const { deepClone } = DU; /* * * * Class * * */ /** * * Class that represents a Highcharts component. * */ class HighchartsComponent extends Component { /* * * * Constructor * * */ /** * Creates a Highcharts component in the cell. * * @param options * The options for the component. */ constructor(cell, options, board) { options = merge(HighchartsComponent.defaultOptions, options); super(cell, options, board); /** * An object of series IDs and their connector handlers. */ this.seriesFromConnector = {}; this.options = options; this.chartConstructor = this.options.chartConstructor || 'chart'; this.type = 'Highcharts'; this.chartContainer = createElement('figure', void 0, void 0, this.contentElement, true); this.setOptions(); this.chartOptions = merge((this.options.chartOptions || { chart: {} }), { tooltip: {} // Temporary fix for #18876 }); this.innerResizeTimeouts = []; } onTableChanged() { this.updateSeries(); } /* * * * Functions * * */ async load() { this.emit({ type: 'load' }); await super.load(); this.emit({ type: 'afterLoad' }); return this; } render() { const hcComponent = this; super.render(); hcComponent.chart = hcComponent.getChart(); hcComponent.updateSeries(); hcComponent.emit({ type: 'afterRender' }); hcComponent.setupConnectorUpdate(); this.sync.start(); return this; } resize(width, height) { this.resizeDynamicContent(width, height); while (this.innerResizeTimeouts.length) { const timeoutID = this.innerResizeTimeouts.pop(); if (timeoutID) { clearTimeout(timeoutID); } } this.innerResizeTimeouts.push(setTimeout(() => { if (this.chart && this.chart.container) { const heightOffset = this.contentElement.offsetHeight - this.chart?.container.offsetHeight; this.chart.setSize(null, (Math.abs(heightOffset) > 1) ? this.contentElement.offsetHeight : null, false); } }, 33)); return this; } /** * Adds call update value in store, when chart's point is updated. * @private */ setupConnectorUpdate() { const { connectorHandlers, chart } = this; if (!chart || !this.options.allowConnectorUpdate) { return; } const seriesLength = chart.series.length; for (let i = 0, iEnd = connectorHandlers.length; i < iEnd; i++) { const connectorHandler = connectorHandlers[i]; for (let j = 0; j < seriesLength; j++) { const series = chart.series[j]; series.update({ point: { events: { update: (e) => { this.onChartUpdate(e.target, connectorHandler); } } } }, false); } } } /** * Update the store, when the point is being dragged. * @param point Dragged point. * @param connectorHandler Connector handler with data to update. */ onChartUpdate(point, connectorHandler) { const table = connectorHandler.dataTable; const columnAssignment = connectorHandler.columnAssignment; const seriesId = point.series.options.id; const converter = new DataConverter(); const valueToSet = DataConverterUtils.asNumber(point.y, converter.decimalRegExp); if (!table) { return; } let columnId; if (columnAssignment && seriesId) { const data = columnAssignment.find((s) => s.seriesId === seriesId)?.data; if (isString(data)) { columnId = data; } else if (Array.isArray(data)) { columnId = data[1]; } else if (data) { columnId = data.y ?? data.value; } } if (!columnId) { columnId = seriesId ?? point.series.name; } table.setCell(columnId, point.index, valueToSet); } /** * Internal method for handling option updates. * @internal */ setOptions() { if (this.options.chartClassName) { this.chartContainer.classList.value = HighchartsComponentDefaults.className + ' ' + this.options.chartClassName; } if (this.options.chartID) { this.chartContainer.id = this.options.chartID; } } /** * Handles updating via options. * * @param options * The options to apply. */ async update(options, shouldRerender = true) { await super.update(options, false); this.setOptions(); if (this.options.chartConstructor !== this.chartConstructor) { this.chartConstructor = this.options.chartConstructor || 'chart'; this.chartOptions = this.options.chartOptions || {}; this.chart?.destroy(); delete this.chart; } else { this.chart?.update(merge(this.options.chartOptions) || {}); } this.emit({ type: 'afterUpdate' }); shouldRerender && this.render(); } /** * Updates chart's series when the data table is changed. * @private */ updateSeries() { const { chart } = this; const connectorHandlers = this.connectorHandlers; if (!chart) { return; } const newSeriesIds = []; for (const connectorHandler of connectorHandlers) { const options = connectorHandler.options; let columnAssignment = options.columnAssignment; if (!columnAssignment && connectorHandler.dataTable) { columnAssignment = this.getDefaultColumnAssignment(connectorHandler.dataTable.getColumnIds(), connectorHandler.dataTable); } if (columnAssignment) { connectorHandler.columnAssignment = columnAssignment; for (const { seriesId } of columnAssignment) { if (seriesId) { newSeriesIds.push(seriesId); } } } } const seriesArray = Object.keys(this.seriesFromConnector); // Remove series that were added in the previous update and are not // present in the new columnAssignment. for (let i = 0, iEnd = seriesArray.length; i < iEnd; ++i) { const oldSeriesId = seriesArray[i]; if (newSeriesIds.some((newSeriesId) => newSeriesId === oldSeriesId)) { continue; } const series = chart.get(oldSeriesId); if (series) { series.destroy(); } } this.seriesFromConnector = {}; for (const connectorHandler of connectorHandlers) { this.updateSeriesFromConnector(connectorHandler); } chart.redraw(); } /** * Updates the series based on the connector from each connector handler. * @param connectorHandler The connector handler. * @private */ updateSeriesFromConnector(connectorHandler) { const chart = this.chart; if (!connectorHandler.connector || !chart || !connectorHandler.dataTable) { return; } const table = connectorHandler.dataTable.getModified(); const modifierOptions = connectorHandler.dataTable.getModifier()?.options; const columnAssignment = connectorHandler.columnAssignment ?? []; // Create the series or update the existing ones. for (let i = 0, iEnd = columnAssignment.length; i < iEnd; ++i) { const assignment = columnAssignment[i]; const dataStructure = assignment.data; const series = chart.get(assignment.seriesId); const seriesOptions = {}; // Prevent dragging on series, which were created out of a // columns which are created by MathModifier. const adjustDraggableOptions = (compare) => { if (modifierOptions?.type === 'Math' && modifierOptions .columnFormulas?.some((formula) => compare(formula.column))) { seriesOptions.dragDrop = { draggableY: false }; } }; // Set the series data based on the column assignment data structure // type. if (isString(dataStructure)) { const column = table.getColumn(dataStructure); if (column) { seriesOptions.data = column.slice(); } adjustDraggableOptions((columnId) => (columnId === dataStructure)); } else if (Array.isArray(dataStructure)) { seriesOptions.data = table.getRows(0, table.rowCount, dataStructure); adjustDraggableOptions((columnId) => (dataStructure.some((name) => name === columnId))); } else { const keys = Object.keys(dataStructure); const columnIds = []; for (let j = 0, jEnd = keys.length; j < jEnd; ++j) { columnIds.push(dataStructure[keys[j]]); } seriesOptions.keys = keys; seriesOptions.data = table.getRows(0, table.rowCount, columnIds); adjustDraggableOptions((columnId) => (columnIds.some((name) => name === columnId))); } if (!series) { chart.addSeries({ name: assignment.seriesId, id: assignment.seriesId, ...seriesOptions }, false); } else { series.update(seriesOptions, false); } this.seriesFromConnector[assignment.seriesId] = connectorHandler; } } /** * Destroy chart and create a new one. * * @returns * The chart. * * @private * */ getChart() { return this.chart || this.createChart(); } /** * Destroys the highcharts component. */ destroy() { // Cleanup references in the global Highcharts scope // Destroy chart before destroying the component element // to ensure chart has access to its renderTo element if (this.chart && this.chart.renderTo && this.chart.renderer) { try { this.chart.destroy(); } catch (e) { // Chart may already be destroyed or renderTo/renderer // eslint-disable-next-line no-console console.warn('Error destroying chart:', e); } this.chart = void 0; } super.destroy(); } /** * Creates default mapping when columnAssignment is not declared. * @param { Array<string>} columnIds all columns returned from dataTable. * * @returns * The record of mapping * * @private * */ getDefaultColumnAssignment(columnIds = [], presentationTable) { const result = []; const firstColumn = presentationTable.getColumn(columnIds[0]); if (firstColumn && isString(firstColumn[0])) { for (let i = 1, iEnd = columnIds.length; i < iEnd; ++i) { result.push({ seriesId: columnIds[i], data: [columnIds[0], columnIds[i]] }); } return result; } for (let i = 0, iEnd = columnIds.length; i < iEnd; ++i) { result.push({ seriesId: columnIds[i], data: columnIds[i] }); } return result; } /** * Creates chart. * * @returns * The chart. * * @private * */ createChart() { const charter = HighchartsComponent.charter || Globals.win.Highcharts; if (!this.chartConstructor) { this.chartConstructor = 'chart'; } const Factory = charter[this.chartConstructor]; if (Factory) { try { if (this.chartConstructor === 'chart') { return charter.Chart.chart(this.chartContainer, this.chartOptions); } return new Factory(this.chartContainer, this.chartOptions); } catch (e) { throw new Error(`The Highcharts component in cell '${this.cell.id}' ` + 'is misconfigured. \n____________\n' + String(e)); } } if (typeof charter.chart !== 'function') { throw new Error('Chart constructor not found'); } return this.chart; } getOptionsOnDrop(sidebar) { const connectorsIds = sidebar.editMode.board.dataPool.getConnectorIds(); let options = { type: 'Highcharts', chartOptions: { chart: { animation: false, type: 'column', zooming: {} } } }; if (connectorsIds.length) { options = { ...options, connector: { id: connectorsIds[0] } }; } return options; } /** * Get the HighchartsComponent component's options. * @returns * HighchartsComponent component's options. * * @internal * */ getOptions() { return { ...diffObjects(this.options, HighchartsComponent.defaultOptions), type: 'Highcharts' }; } /** * Retrieves editable options for the chart. * * @returns * The editable options for the chart and its values. */ getEditableOptions() { const component = this; const componentOptions = component.options; const chart = component.chart; const chartOptions = chart && chart.options; const chartType = chartOptions?.chart?.type || 'line'; return deepClone(merge({ chartOptions }, { chartOptions: { yAxis: splat(chart && chart.yAxis[0].options), xAxis: splat(chart && chart.xAxis[0].options), plotOptions: { series: ((chartOptions && chartOptions.plotOptions) || {})[chartType] } } }, componentOptions), ['dataTable', 'points', 'series', 'data', 'editableOptions']); } getEditableOptionValue(propertyPath) { const component = this; if (!propertyPath) { return; } if (propertyPath.length === 1 && propertyPath[0] === 'chartOptions') { return JSON.stringify(component.options.chartOptions, null, 2); } return super.getEditableOptionValue.call(this, propertyPath); } } /** * Predefined sync config for Highcharts component. */ HighchartsComponent.predefinedSyncConfig = HighchartsSyncs; /** * Default options of the Highcharts component. */ HighchartsComponent.defaultOptions = merge(Component.defaultOptions, HighchartsComponentDefaults); /* * * * Default Export * * */ export default HighchartsComponent;