UNPKG

@highcharts/dashboards

Version:
532 lines (531 loc) 18.5 kB
/* * * * (c) 2009-2024 Highsoft AS * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * Authors: * - Sophie Bremer * * */ 'use strict'; import Component from '../Components/Component.js'; import DataModifier from '../../Data/Modifiers/DataModifier.js'; const { Range: RangeModifier } = DataModifier.types; import Globals from '../Globals.js'; import NavigatorComponentDefaults from './NavigatorComponentDefaults.js'; import U from '../../Core/Utilities.js'; const { addEvent, diffObjects, isNumber, isObject, isString, merge, pick } = U; /* * * * Constants * * */ const navigatorComponentSync = { crossfilter: { emitter: crossfilterEmitter }, extremes: { emitter: extremesEmitter, handler: extremesReceiver } }; /* * * * Functions * * */ /** @internal */ function crossfilterEmitter() { const component = this; const afterSetExtremes = async (extremes) => { if (component.connector) { const table = component.connector.table, dataCursor = component.board.dataCursor, filterColumn = component.getColumnAssignment()[0], [min, max] = component.getAxisExtremes(); let modifier = table.getModifier(); if (modifier instanceof RangeModifier) { setRangeOptions(modifier.options.ranges, filterColumn, min, max); } else { modifier = new RangeModifier({ ranges: [{ column: filterColumn, maxValue: max, minValue: min }] }); } await table.setModifier(modifier); dataCursor.emitCursor(table, { type: 'range', columns: [filterColumn], firstRow: 0, lastRow: table.getRowCount() - 1, state: 'crossfilter' }, extremes); } }; let delay; return addEvent(component.chart.xAxis[0], 'afterSetExtremes', function (extremes) { clearTimeout(delay); delay = setTimeout(afterSetExtremes, 50, this, extremes); }); } /** @internal */ function extremesEmitter() { const component = this; const afterSetExtremes = (extremes) => { if (component.connector) { const table = component.connector.table, dataCursor = component.board.dataCursor, filterColumn = component.getColumnAssignment()[0], [min, max] = component.getAxisExtremes(); dataCursor.emitCursor(table, { type: 'position', column: filterColumn, row: table.getRowIndexBy(filterColumn, min), state: 'xAxis.extremes.min' }, extremes); dataCursor.emitCursor(table, { type: 'position', column: filterColumn, row: table.getRowIndexBy(filterColumn, max), state: 'xAxis.extremes.max' }, extremes); } }; let delay; return addEvent(component.chart.xAxis[0], 'afterSetExtremes', function (extremes) { clearTimeout(delay); delay = setTimeout(afterSetExtremes, 50, this, extremes); }); } /** @internal */ function extremesReceiver() { const component = this, dataCursor = component.board.dataCursor; const extremesListener = (e) => { const cursor = e.cursor; if (!component.connector) { return; } const table = component.connector.table; // assume first column with unique keys as fallback let extremesColumn = table.getColumnNames()[0], maxIndex = table.getRowCount(), minIndex = 0; if (cursor.type === 'range') { maxIndex = cursor.lastRow; minIndex = cursor.firstRow; if (cursor.columns) { extremesColumn = pick(cursor.columns[0], extremesColumn); } } else if (cursor.state === 'xAxis.extremes.max') { extremesColumn = pick(cursor.column, extremesColumn); maxIndex = pick(cursor.row, maxIndex); } else { extremesColumn = pick(cursor.column, extremesColumn); minIndex = pick(cursor.row, minIndex); } const modifier = table.getModifier(); if (typeof extremesColumn === 'string' && modifier instanceof RangeModifier) { const ranges = modifier.options.ranges, min = table.getCell(extremesColumn, minIndex), max = table.getCell(extremesColumn, maxIndex); if (max !== null && typeof max !== 'undefined' && min !== null && typeof min !== 'undefined') { unsetRangeOptions(ranges, extremesColumn); ranges.unshift({ column: extremesColumn, maxValue: max, minValue: min }); table.setModifier(modifier); } } }; const registerCursorListeners = () => { const table = component.connector && component.connector.table; if (table) { dataCursor.addListener(table.id, 'xAxis.extremes', extremesListener); dataCursor.addListener(table.id, 'xAxis.extremes.max', extremesListener); dataCursor.addListener(table.id, 'xAxis.extremes.min', extremesListener); } }; const unregisterCursorListeners = () => { const table = component.connector && component.connector.table; if (table) { dataCursor.removeListener(table.id, 'xAxis.extremes', extremesListener); dataCursor.removeListener(table.id, 'xAxis.extremes.max', extremesListener); dataCursor.removeListener(table.id, 'xAxis.extremes.min', extremesListener); } }; registerCursorListeners(); component.on('setConnector', () => unregisterCursorListeners()); component.on('afterSetConnector', () => registerCursorListeners()); } /** @internal */ function setRangeOptions(ranges, column, minValue, maxValue) { let changed = false; for (let i = 0, iEnd = ranges.length; i < iEnd; ++i) { if (ranges[i].column === column) { ranges[i].maxValue = maxValue; ranges[i].minValue = minValue; changed = true; break; } } if (!changed) { ranges.push({ column, maxValue, minValue }); } } /** @internal */ function unsetRangeOptions(ranges, column) { for (let i = 0, iEnd = ranges.length; i < iEnd; ++i) { if (ranges[i].column === column) { return ranges.splice(i, 1)[0]; } } } /* * * * Class * * */ /** * Setup a component with data navigation. */ class NavigatorComponent extends Component { /* * * * Static Functions * * */ /** * Creates component from JSON. * * @param json * Set of component options, used for creating the Highcharts component. * * @returns * Highcharts component based on config from JSON. * * @private */ static fromJSON(json, cell) { const options = json.options, component = new NavigatorComponent(cell, options); component.emit({ type: 'fromJSON', json }); return component; } /* * * * Constructor * * */ constructor(cell, options) { super(cell, options); this.options = merge(NavigatorComponent.defaultOptions, options); const charter = (NavigatorComponent.charter.Chart || Globals.win.Highcharts); this.chartContainer = Globals.win.document.createElement('div'); this.chart = charter .chart(this.chartContainer, (this.options.chartOptions || {})); this.chartContainer.classList .add(Globals.classNamePrefix + 'navigator'); this.filterAndAssignSyncOptions(navigatorComponentSync); this.sync = new NavigatorComponent.Sync(this, this.syncHandlers); const crossfilterOptions = this.options.sync?.crossfilter; if (crossfilterOptions === true || (isObject(crossfilterOptions) && crossfilterOptions.enabled)) { this.chart.update(merge({ navigator: { xAxis: { labels: { format: '{value}' } } } }, this.options.chartOptions || {}), false); } } /* * * * Functions * * */ /** @private */ adjustNavigator() { const chart = this.chart, height = pick(chart.chartHeight, this.contentElement.clientHeight), width = this.contentElement.clientWidth, chartUpdates = {}; if (chart.chartHeight !== height || chart.chartWidth !== width) { chartUpdates.chart = { height, width }; } if (chart.navigator) { const navigator = chart.navigator, navigatorHeight = (navigator.top - chart.plotTop + navigator.height); if (navigator.height !== navigatorHeight) { chartUpdates.navigator = { handles: { height: Math.round(height / 4) }, height: navigatorHeight }; } if (Object.keys(chartUpdates).length) { chart.update(chartUpdates, false); } if (navigator.series && navigator.series[0]) { navigator.series[0].update({ type: chart.series[0].type }, false); } } else if (Object.keys(chartUpdates).length) { chart.update(chartUpdates, false); } } /** * Returns the first column of columnAssignments to use for navigator data. * @private * * @return * Navigator column assignment. */ getColumnAssignment() { const columnAssignments = (this.options.columnAssignments || {}); let columnsAssignment; for (const column of Object.keys(columnAssignments)) { columnsAssignment = columnAssignments[column]; if (columnsAssignment !== null) { return [column, columnsAssignment]; } } if (this.connector) { const columns = this.connector.table.getColumnNames(); if (columns.length) { return [columns[0], 'y']; } } return ['', 'y']; } /** * Gets the component's options. * @internal */ getOptions() { return { ...diffObjects(this.options, NavigatorComponentDefaults), type: 'Navigator' }; } /** * Gets the extremes of the navigator's x-axis. */ getAxisExtremes() { const axis = this.chart.xAxis[0], extremes = axis.getExtremes(), min = isNumber(extremes.min) ? extremes.min : extremes.dataMin, max = isNumber(extremes.max) ? extremes.max : extremes.dataMax; if (this.categories) { return [ this.categories[Math.max(0, Math.ceil(min))], this.categories[Math.min(this.categories.length - 1, Math.floor(max))] ]; } if (axis.hasNames) { return [ axis.names[Math.ceil(min)], axis.names[Math.floor(max)] ]; } return [min, max]; } /** @private */ async load() { await super.load(); this.contentElement.appendChild(this.chartContainer); this.parentElement.appendChild(this.element); this.adjustNavigator(); this.emit({ type: 'afterLoad' }); return this; } onTableChanged() { this.renderNavigator(); } /** @private */ redrawNavigator() { const timeouts = this.resizeTimeouts; for (let i = 0, iEnd = timeouts.length; i < iEnd; ++i) { clearTimeout(timeouts[i]); } timeouts.length = 0; timeouts.push(setTimeout(() => { this.adjustNavigator(); this.chart.redraw(); }, 33)); } /** @private */ render() { const component = this; super.render(); component.renderNavigator(); component.sync.start(); component.emit({ type: 'afterRender' }); return component; } /** @private */ renderNavigator() { const chart = this.chart; if (this.connector) { const table = this.connector.table, options = this.options, column = this.getColumnAssignment(), columnValues = table.getColumn(column[0], true) || [], crossfilterOptions = options.sync?.crossfilter; let data; if (crossfilterOptions === true || (isObject(crossfilterOptions) && crossfilterOptions.enabled)) { data = this.generateCrossfilterData(); } else { data = columnValues.slice(); } if (!chart.series[0]) { chart.addSeries({ id: table.id, data }, false); } else { chart.series[0].setData(data, false); } } this.redrawNavigator(); } /** * Generates the data for the crossfilter navigator. */ generateCrossfilterData() { let crossfilterOptions = this.options.sync?.crossfilter; const table = this.connector?.table; const columnValues = table?.getColumn(this.getColumnAssignment()[0], true) || []; // TODO: Remove this when merging to v2. if (crossfilterOptions === true) { crossfilterOptions = { affectNavigator: false }; } if (!table || columnValues.length < 1 || !isObject(crossfilterOptions)) { return []; } const values = []; const uniqueXValues = []; for (let i = 0, iEnd = columnValues.length; i < iEnd; i++) { let value = columnValues[i]; if (value === null) { continue; } else if (!isNumber(value)) { value = `${value}`; } // Check if the x-axis data is not of mixed type. if (this.stringData === void 0) { this.stringData = isString(value); } else if (this.stringData !== isString(value)) { throw new Error('Mixed data types in crossfilter navigator are ' + 'not supported.'); } values.push(value); if (uniqueXValues.indexOf(value) === -1) { uniqueXValues.push(value); } } uniqueXValues.sort((a, b) => (pick(a, NaN) < pick(b, NaN) ? -1 : a === b ? 0 : 1)); let filteredValues; const modifierOptions = table.getModifier()?.options; if (crossfilterOptions.affectNavigator && modifierOptions) { const appliedRanges = [], rangedColumns = [], { ranges } = modifierOptions; for (let i = 0, iEnd = ranges.length; i < iEnd; i++) { if (ranges[i].column !== this.getColumnAssignment()[0]) { appliedRanges.push(ranges[i]); rangedColumns.push(table.getColumn(ranges[i].column, true) || []); } } filteredValues = []; const appliedRagesLength = appliedRanges.length; for (let i = 0, iEnd = values.length; i < iEnd; i++) { const value = values[i]; let allConditionsMet = true; for (let j = 0; j < appliedRagesLength; j++) { const range = appliedRanges[j]; if (!(rangedColumns[j][i] >= (range.minValue ?? -Infinity) && rangedColumns[j][i] <= (range.maxValue ?? Infinity))) { allConditionsMet = false; break; } } if (allConditionsMet) { filteredValues.push(value); } } } else { filteredValues = values; } const seriesData = []; if (this.stringData) { this.categories = uniqueXValues; for (let i = 0, iEnd = uniqueXValues.length; i < iEnd; i++) { seriesData.push([i, null]); } } else { for (let i = 0, iEnd = uniqueXValues.length; i < iEnd; i++) { seriesData.push([uniqueXValues[i], null]); } } for (let i = 0, iEnd = filteredValues.length; i < iEnd; i++) { const index = uniqueXValues.indexOf(filteredValues[i]); seriesData[index][1] = (seriesData[index][1] || 0) + 1; } return seriesData; } /** @private */ resize(width, height) { super.resize(width, height); this.redrawNavigator(); return this; } /** * Handles updating via options. * * @param options * The options to apply. */ async update(options, shouldRerender = true) { const chart = this.chart, crossfilterOptions = this.options.sync?.crossfilter; await super.update(options, false); if (options.sync) { this.filterAndAssignSyncOptions(navigatorComponentSync); } if (options.chartOptions) { chart.update(merge((crossfilterOptions === true || (isObject(crossfilterOptions) && crossfilterOptions.enabled) ? { navigator: { xAxis: { labels: { format: '{value}' } } } } : {}), options.chartOptions), false); } this.emit({ type: 'afterUpdate' }); if (shouldRerender) { this.render(); } } getOptionsOnDrop( // eslint-disable-next-line @typescript-eslint/no-unused-vars sidebar) { return {}; } } /** * Default options of the Navigator component. */ NavigatorComponent.defaultOptions = merge(Component.defaultOptions, NavigatorComponentDefaults); /* * * * Default Export * * */ export default NavigatorComponent;