@highcharts/dashboards
Version:
Highcharts Dashboards framework
323 lines (322 loc) • 10.9 kB
JavaScript
/* *
*
* (c) 2009-2025 Highsoft AS
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* Authors:
* - Sophie Bremer
*
* */
'use strict';
import Component from '../Component.js';
import Globals from '../../Globals.js';
import NavigatorComponentDefaults from './NavigatorComponentDefaults.js';
import NavigatorSyncs from './NavigatorSyncs/NavigatorSyncs.js';
import U from '../../../Core/Utilities.js';
const { diffObjects, isNumber, isString, merge, pick } = U;
/* *
*
* Class
*
* */
/**
* Setup a component with data navigation.
*/
class NavigatorComponent extends Component {
/* *
*
* Constructor
*
* */
constructor(cell, options) {
super(cell, options);
this.type = 'Navigator';
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');
if (this.sync.syncConfig.crossfilter?.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 columnAssignment to use for navigator data.
* @private
*
* @return
* Navigator column assignment.
*/
getColumnAssignment() {
const columnAssignment = this.options.columnAssignment ??
this.options.columnAssignments ?? {};
let columnsAssignment;
for (const column of Object.keys(columnAssignment)) {
columnsAssignment = columnAssignment[column];
if (columnsAssignment !== null) {
return [column, columnsAssignment];
}
}
const connector = this.getFirstConnector();
if (connector) {
const columns = 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;
const connector = this.getFirstConnector();
if (connector) {
const table = connector.table, column = this.getColumnAssignment(), columnValues = table.getColumn(column[0], true) || [];
let data;
if (this.sync.syncConfig.crossfilter?.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() {
const crossfilterOptions = this.sync.syncConfig.crossfilter;
const table = this.getFirstConnector()?.table;
const columnValues = table?.getColumn(this.getColumnAssignment()[0], true) || [];
if (!table || columnValues.length < 1 || !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;
await super.update(options, false);
if (options.chartOptions) {
chart.update(merge(this.sync.syncConfig.crossfilter?.enabled ? ({ navigator: { xAxis: { labels: { format: '{value}' } } } }) : {}, options.chartOptions), false);
}
this.emit({ type: 'afterUpdate' });
if (shouldRerender) {
this.render();
}
}
getOptionsOnDrop() {
return {};
}
}
/**
* Default options of the Navigator component.
*/
NavigatorComponent.defaultOptions = merge(Component.defaultOptions, NavigatorComponentDefaults);
/**
* Predefined sync configuration for the Navigator component.
*/
NavigatorComponent.predefinedSyncConfig = NavigatorSyncs;
/* *
*
* Default Export
*
* */
export default NavigatorComponent;