Browse Source

UI: Implement comparison support for new time series charts.

pull/10562/head
Igor Kulikov 2 years ago
parent
commit
3e527f2634
  1. 9
      ui-ngx/src/app/core/api/widget-api.models.ts
  2. 14
      ui-ngx/src/app/core/api/widget-subscription.ts
  3. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts
  4. 219
      ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts
  5. 198
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
  6. 142
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
  7. 12
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts
  8. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts
  9. 16
      ui-ngx/src/app/shared/models/widget.models.ts

9
ui-ngx/src/app/core/api/widget-api.models.ts

@ -30,7 +30,12 @@ import {
import { TimeService } from '../services/time.service';
import { DeviceService } from '../http/device.service';
import { UtilsService } from '@core/services/utils.service';
import { SubscriptionTimewindow, Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
import {
ComparisonDuration,
SubscriptionTimewindow,
Timewindow,
WidgetTimewindow
} from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models';
import { HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service';
@ -265,7 +270,7 @@ export interface WidgetSubscriptionOptions {
onTimewindowChangeFunction?: (timewindow: Timewindow) => Timewindow;
legendConfig?: LegendConfig;
comparisonEnabled?: boolean;
timeForComparison?: moment_.unitOfTime.DurationConstructor;
timeForComparison?: ComparisonDuration;
comparisonCustomIntervalValue?: number;
decimals?: number;
units?: string;

14
ui-ngx/src/app/core/api/widget-subscription.ts

@ -23,13 +23,13 @@ import {
WidgetSubscriptionOptions
} from '@core/api/widget-api.models';
import {
DataKey,
DataKey, DataKeySettingsWithComparison,
DataSet,
DataSetHolder,
Datasource,
DatasourceData,
datasourcesHasAggregation,
DatasourceType,
DatasourceType, isDataKeySettingsWithComparison,
LegendConfig,
LegendData,
LegendKey,
@ -513,7 +513,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.configuredDatasources.forEach((datasource, datasourceIndex) => {
const additionalDataKeys: DataKey[] = [];
datasource.dataKeys.forEach((dataKey, dataKeyIndex) => {
if (dataKey.settings.comparisonSettings && dataKey.settings.comparisonSettings.showValuesForComparison) {
if (isDataKeySettingsWithComparison(dataKey.settings) && dataKey.settings.comparisonSettings.showValuesForComparison) {
const additionalDataKey = deepClone(dataKey);
additionalDataKey.isAdditional = true;
additionalDataKey.origDataKeyIndex = dataKeyIndex;
@ -1468,11 +1468,12 @@ export class WidgetSubscription implements IWidgetSubscription {
if (datasource.isAdditional) {
const origDatasource = this.datasourcePages[datasource.origDatasourceIndex].data[dIndex];
datasource.dataKeys.forEach((dataKey) => {
if (dataKey.settings.comparisonSettings.color) {
const settings: DataKeySettingsWithComparison = dataKey.settings;
if (settings.comparisonSettings.color) {
dataKey.color = dataKey.settings.comparisonSettings.color;
}
const origDataKey = origDatasource.dataKeys[dataKey.origDataKeyIndex];
origDataKey.settings.comparisonSettings.color = dataKey.color;
(origDataKey.settings as DataKeySettingsWithComparison).comparisonSettings.color = dataKey.color;
});
}
});
@ -1523,7 +1524,8 @@ export class WidgetSubscription implements IWidgetSubscription {
const formattedData = flatFormattedData(formattedDataArray);
datasource.dataKeys.forEach((dataKey) => {
if (this.comparisonEnabled && dataKey.isAdditional && dataKey.settings.comparisonSettings.comparisonValuesLabel) {
if (this.comparisonEnabled && dataKey.isAdditional && isDataKeySettingsWithComparison(dataKey.settings) &&
dataKey.settings.comparisonSettings.comparisonValuesLabel) {
dataKey.label = createLabelFromPattern(dataKey.settings.comparisonSettings.comparisonValuesLabel, formattedData);
} else {
if (this.comparisonEnabled && dataKey.isAdditional) {

9
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts

@ -131,6 +131,11 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
yAxes: [settings.yAxes, []],
series: [this.getSeries(configData.config.datasources), []],
comparisonEnabled: [settings.comparisonEnabled, []],
timeForComparison: [settings.timeForComparison, []],
comparisonCustomIntervalValue: [settings.comparisonCustomIntervalValue, [Validators.min(0)]],
thresholds: [settings.thresholds, []],
showTitle: [configData.config.showTitle, []],
@ -201,6 +206,10 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.comparisonEnabled = config.comparisonEnabled;
this.widgetConfig.config.settings.timeForComparison = config.timeForComparison;
this.widgetConfig.config.settings.comparisonCustomIntervalValue = config.comparisonCustomIntervalValue;
this.widgetConfig.config.settings.thresholds = config.thresholds;
this.widgetConfig.config.settings.dataZoom = config.dataZoom;

219
ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts

@ -115,6 +115,7 @@ export type EChartsSeriesItem = {
decimals?: number;
latestData?: FormattedData;
tooltipValueFormatFunction?: EChartsTooltipValueFormatFunction;
comparisonItem?: boolean;
};
export enum EChartsShape {
@ -200,9 +201,9 @@ export const timeAxisBandWidthCalculator: TimeAxisBandWidthCalculator = (model)
}
};
export const getXAxis = (chart: ECharts): Axis2D => {
export const getAxis = (chart: ECharts, mainType: string, axisId: string): Axis2D => {
const model: GlobalModel = (chart as any).getModel();
const models = model.queryComponents({mainType: 'xAxis'});
const models = model.queryComponents({mainType, id: axisId});
if (models?.length) {
const axisModel = models[0] as AxisModel;
return axisModel.axis;
@ -210,27 +211,20 @@ export const getXAxis = (chart: ECharts): Axis2D => {
return null;
};
export const getYAxis = (chart: ECharts, axisId: string): Axis2D => {
const model: GlobalModel = (chart as any).getModel();
const models = model.queryComponents({mainType: 'yAxis', id: axisId});
if (models?.length) {
const axisModel = models[0] as AxisModel;
return axisModel.axis;
}
return null;
};
export const calculateYAxisWidth = (chart: ECharts, axisId: string): number => {
const axis = getYAxis(chart, axisId);
return calculateAxisSize(axis);
export const calculateAxisSize = (chart: ECharts, mainType: string, axisId: string): number => {
const axis = getAxis(chart, mainType, axisId);
return _calculateAxisSize(axis);
};
export const calculateXAxisHeight = (chart: ECharts): number => {
const axis = getXAxis(chart);
return calculateAxisSize(axis);
export const measureAxisNameSize = (chart: ECharts, mainType: string, axisId: string, name: string): number => {
const axis = getAxis(chart, mainType, axisId);
if (axis) {
return axis.model.getModel('nameTextStyle').getTextRect(name).height;
}
return 0;
};
const calculateAxisSize = (axis: Axis2D): number => {
const _calculateAxisSize = (axis: Axis2D): number => {
let size = 0;
if (axis && axis.model.option.show) {
const labelUnionRect = estimateLabelUnionRect(axis);
@ -247,22 +241,6 @@ const calculateAxisSize = (axis: Axis2D): number => {
return size;
};
export const measureYAxisNameWidth = (chart: ECharts, axisId: string, name: string): number => {
const axis = getYAxis(chart, axisId);
if (axis) {
return axis.model.getModel('nameTextStyle').getTextRect(name).height;
}
return 0;
};
export const measureXAxisNameHeight = (chart: ECharts, name: string): number => {
const axis = getXAxis(chart);
if (axis) {
return axis.model.getModel('nameTextStyle').getTextRect(name).height;
}
return 0;
};
const measureSymbolOffset = (symbol: string, symbolSize: any): number => {
if (isNumber(symbolSize)) {
if (symbol) {
@ -280,7 +258,7 @@ const measureSymbolOffset = (symbol: string, symbolSize: any): number => {
export const measureThresholdOffset = (chart: ECharts, axisId: string, thresholdId: string, value: any): [number, number] => {
const offset: [number, number] = [0,0];
const axis = getYAxis(chart, axisId);
const axis = getAxis(chart, 'yAxis', axisId);
if (axis && !axis.scale.isBlank()) {
const extent = axis.scale.getExtent();
const model: GlobalModel = (chart as any).getModel();
@ -352,7 +330,7 @@ export const measureThresholdOffset = (chart: ECharts, axisId: string, threshold
};
export const getAxisExtent = (chart: ECharts, axisId: string): [number, number] => {
const axis = getYAxis(chart, axisId);
const axis = getAxis(chart, 'yAxis', axisId);
if (axis) {
return axis.scale.getExtent();
}
@ -503,43 +481,72 @@ export const echartsTooltipFormatter = (renderer: Renderer2,
focusedSeriesIndex: number,
series?: EChartsSeriesItem[],
interval?: Interval): null | HTMLElement => {
if (!params || Array.isArray(params) && !params[0]) {
return null;
}
const firstParam = Array.isArray(params) ? params[0] : params;
if (!firstParam.value) {
const tooltipParams = mapTooltipParams(params, series, focusedSeriesIndex);
if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) {
return null;
}
const tooltipElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(tooltipElement, 'display', 'flex');
renderer.setStyle(tooltipElement, 'flex-direction', 'column');
renderer.setStyle(tooltipElement, 'align-items', 'flex-start');
renderer.setStyle(tooltipElement, 'gap', '4px');
if (settings.tooltipShowDate) {
const dateElement: HTMLElement = renderer.createElement('div');
let dateText: string;
const startTs = firstParam.value[2];
const endTs = firstParam.value[3];
if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) {
const startDateText = tooltipDateFormat.update(startTs, interval);
const endDateText = tooltipDateFormat.update(endTs - 1, interval);
if (startDateText === endDateText) {
dateText = startDateText;
} else {
dateText = startDateText + ' - ' + endDateText;
}
} else {
const ts = firstParam.value[0];
dateText = tooltipDateFormat.update(ts, interval);
renderer.setStyle(tooltipElement, 'gap', '16px');
buildItemsTooltip(tooltipElement, tooltipParams.items, renderer, tooltipDateFormat, settings, valueFormatFunction, interval);
buildItemsTooltip(tooltipElement, tooltipParams.comparisonItems, renderer, tooltipDateFormat, settings, valueFormatFunction, interval);
return tooltipElement;
};
interface TooltipItem {
param: CallbackDataParams;
dataItem: EChartsSeriesItem;
}
interface TooltipParams {
items: TooltipItem[];
comparisonItems: TooltipItem[];
}
const buildItemsTooltip = (tooltipElement: HTMLElement,
items: TooltipItem[],
renderer: Renderer2,
tooltipDateFormat: DateFormatProcessor,
settings: EChartsTooltipWidgetSettings,
valueFormatFunction: EChartsTooltipValueFormatFunction,
interval?: Interval) => {
if (items.length) {
const tooltipItemsElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(tooltipItemsElement, 'display', 'flex');
renderer.setStyle(tooltipItemsElement, 'flex-direction', 'column');
renderer.setStyle(tooltipItemsElement, 'align-items', 'flex-start');
renderer.setStyle(tooltipItemsElement, 'gap', '4px');
renderer.appendChild(tooltipElement, tooltipItemsElement);
if (settings.tooltipShowDate) {
renderer.appendChild(tooltipItemsElement,
constructEchartsTooltipDateElement(renderer, tooltipDateFormat, settings, items[0].param, interval));
}
for (const item of items) {
renderer.appendChild(tooltipItemsElement,
constructEchartsTooltipSeriesElement(renderer, settings, item, valueFormatFunction));
}
renderer.appendChild(dateElement, renderer.createText(dateText));
renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family);
renderer.setStyle(dateElement, 'font-size', settings.tooltipDateFont.size + settings.tooltipDateFont.sizeUnit);
renderer.setStyle(dateElement, 'font-style', settings.tooltipDateFont.style);
renderer.setStyle(dateElement, 'font-weight', settings.tooltipDateFont.weight);
renderer.setStyle(dateElement, 'line-height', settings.tooltipDateFont.lineHeight);
renderer.setStyle(dateElement, 'color', settings.tooltipDateColor);
renderer.appendChild(tooltipElement, dateElement);
}
};
const mapTooltipParams = (params: CallbackDataParams[] | CallbackDataParams,
series?: EChartsSeriesItem[],
focusedSeriesIndex?: number): TooltipParams => {
const result: TooltipParams = {
items: [],
comparisonItems: []
};
if (!params || Array.isArray(params) && !params[0]) {
return result;
}
const firstParam = Array.isArray(params) ? params[0] : params;
if (!firstParam.value) {
return result;
}
let seriesParams: CallbackDataParams = null;
if (Array.isArray(params) && focusedSeriesIndex > -1) {
@ -548,22 +555,63 @@ export const echartsTooltipFormatter = (renderer: Renderer2,
seriesParams = params;
}
if (seriesParams) {
renderer.appendChild(tooltipElement,
constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, valueFormatFunction, series));
appendTooltipItem(result, seriesParams, series);
} else if (Array.isArray(params)) {
for (seriesParams of params) {
renderer.appendChild(tooltipElement,
constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, valueFormatFunction, series));
appendTooltipItem(result, seriesParams, series);
}
}
return tooltipElement;
return result;
};
const appendTooltipItem = (tooltipParams: TooltipParams, seriesParams: CallbackDataParams, series?: EChartsSeriesItem[]) => {
const dataItem = series?.find(s => s.id === seriesParams.seriesId);
const tooltipItem: TooltipItem = {
param: seriesParams,
dataItem
};
if (dataItem?.comparisonItem) {
tooltipParams.comparisonItems.push(tooltipItem);
} else {
tooltipParams.items.push(tooltipItem);
}
};
const constructEchartsTooltipDateElement = (renderer: Renderer2,
tooltipDateFormat: DateFormatProcessor,
settings: EChartsTooltipWidgetSettings,
param: CallbackDataParams,
interval?: Interval): HTMLElement => {
const dateElement: HTMLElement = renderer.createElement('div');
let dateText: string;
const startTs = param.value[2];
const endTs = param.value[3];
if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) {
const startDateText = tooltipDateFormat.update(startTs, interval);
const endDateText = tooltipDateFormat.update(endTs - 1, interval);
if (startDateText === endDateText) {
dateText = startDateText;
} else {
dateText = startDateText + ' - ' + endDateText;
}
} else {
const ts = param.value[0];
dateText = tooltipDateFormat.update(ts, interval);
}
renderer.appendChild(dateElement, renderer.createText(dateText));
renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family);
renderer.setStyle(dateElement, 'font-size', settings.tooltipDateFont.size + settings.tooltipDateFont.sizeUnit);
renderer.setStyle(dateElement, 'font-style', settings.tooltipDateFont.style);
renderer.setStyle(dateElement, 'font-weight', settings.tooltipDateFont.weight);
renderer.setStyle(dateElement, 'line-height', settings.tooltipDateFont.lineHeight);
renderer.setStyle(dateElement, 'color', settings.tooltipDateColor);
return dateElement;
};
const constructEchartsTooltipSeriesElement = (renderer: Renderer2,
settings: EChartsTooltipWidgetSettings,
seriesParams: CallbackDataParams,
valueFormatFunction: EChartsTooltipValueFormatFunction,
series?: EChartsSeriesItem[]): HTMLElement => {
item: TooltipItem,
valueFormatFunction: EChartsTooltipValueFormatFunction): HTMLElement => {
const labelValueElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(labelValueElement, 'display', 'flex');
renderer.setStyle(labelValueElement, 'flex-direction', 'row');
@ -579,10 +627,10 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2,
renderer.setStyle(circleElement, 'width', '8px');
renderer.setStyle(circleElement, 'height', '8px');
renderer.setStyle(circleElement, 'border-radius', '50%');
renderer.setStyle(circleElement, 'background', seriesParams.color);
renderer.setStyle(circleElement, 'background', item.param.color);
renderer.appendChild(labelElement, circleElement);
const labelTextElement: HTMLElement = renderer.createElement('div');
renderer.appendChild(labelTextElement, renderer.createText(seriesParams.seriesName));
renderer.appendChild(labelTextElement, renderer.createText(item.param.seriesName));
renderer.setStyle(labelTextElement, 'font-family', 'Roboto');
renderer.setStyle(labelTextElement, 'font-size', '12px');
renderer.setStyle(labelTextElement, 'font-style', 'normal');
@ -596,21 +644,18 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2,
let latestData: FormattedData;
let units = '';
let decimals = 0;
if (series) {
const item = series.find(s => s.id === seriesParams.seriesId);
if (item) {
if (item.tooltipValueFormatFunction) {
formatFunction = item.tooltipValueFormatFunction;
}
latestData = item.latestData;
units = item.units;
decimals = item.decimals;
if (item.dataItem) {
if (item.dataItem.tooltipValueFormatFunction) {
formatFunction = item.dataItem.tooltipValueFormatFunction;
}
latestData = item.dataItem.latestData;
units = item.dataItem.units;
decimals = item.dataItem.decimals;
}
if (!latestData) {
latestData = {} as FormattedData;
}
const value = formatFunction(seriesParams.value[1], latestData, units, decimals);
const value = formatFunction(item.param.value[1], latestData, units, decimals);
renderer.appendChild(valueElement, renderer.createText(value));
renderer.setStyle(valueElement, 'flex', '1');
renderer.setStyle(valueElement, 'text-align', 'end');

198
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts

@ -62,13 +62,15 @@ import {
BarVisualSettings,
renderTimeSeriesBar
} from '@home/components/widget/lib/chart/time-series-chart-bar.models';
import { DataKey } from '@shared/models/widget.models';
import { DataKey, DataKeySettingsWithComparison, WidgetComparisonSettings } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TbColorScheme } from '@shared/models/color.models';
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { MarkLine2DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel';
import { DatePipe } from '@angular/common';
import { BuiltinTextPosition } from 'zrender/src/core/types';
import { CartesianAxisOption } from 'echarts/types/src/coord/cartesian/AxisModel';
import { WidgetTimewindow } from '@shared/models/time/time.models';
export enum TimeSeriesChartType {
default = 'default',
@ -692,7 +694,11 @@ export const timeSeriesChartStateValidator = (control: AbstractControl): Validat
return null;
};
export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings {
export interface TimeSeriesChartComparisonSettings extends WidgetComparisonSettings {
comparisonXAxis?: TimeSeriesChartXAxisSettings;
}
export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings, TimeSeriesChartComparisonSettings {
thresholds: TimeSeriesChartThreshold[];
darkMode: boolean;
dataZoom: boolean;
@ -750,7 +756,13 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = {
tooltipDateColor: 'rgba(0, 0, 0, 0.76)',
tooltipDateInterval: true,
tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)',
tooltipBackgroundBlur: 4
tooltipBackgroundBlur: 4,
comparisonEnabled: false,
timeForComparison: 'previousInterval',
comparisonCustomIntervalValue: 7200000,
comparisonXAxis: mergeDeep({} as TimeSeriesChartXAxisSettings,
defaultTimeSeriesChartXAxisSettings,
{ position: AxisPosition.top } as TimeSeriesChartXAxisSettings)
};
export interface SeriesFillSettings {
@ -798,7 +810,7 @@ export interface BarSeriesSettings {
backgroundSettings: SeriesFillSettings;
}
export interface TimeSeriesChartKeySettings {
export interface TimeSeriesChartKeySettings extends DataKeySettingsWithComparison {
yAxisId: TimeSeriesChartYAxisId;
showInLegend: boolean;
dataHiddenByDefault: boolean;
@ -870,10 +882,16 @@ export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = {
end: 0
}
}
},
comparisonSettings: {
showValuesForComparison: false,
comparisonValuesLabel: '',
color: ''
}
};
export interface TimeSeriesChartDataItem extends EChartsSeriesItem {
xAxisIndex: number;
yAxisId: TimeSeriesChartYAxisId;
yAxisIndex: number;
option?: LineSeriesOption | CustomSeriesOption;
@ -894,13 +912,23 @@ export interface TimeSeriesChartThresholdItem {
option?: LineSeriesOption;
}
export interface TimeSeriesChartYAxis {
export interface TimeSeriesChartAxis {
id: string;
settings: TimeSeriesChartAxisSettings;
option: CartesianAxisOption;
}
export interface TimeSeriesChartYAxis extends TimeSeriesChartAxis {
decimals: number;
settings: TimeSeriesChartYAxisSettings;
option: YAXisOption & ValueAxisBaseOption;
}
export interface TimeSeriesChartXAxis extends TimeSeriesChartAxis {
settings: TimeSeriesChartXAxisSettings;
option: XAXisOption;
}
export const createTimeSeriesYAxis = (units: string,
decimals: number,
settings: TimeSeriesChartYAxisSettings,
@ -944,6 +972,7 @@ export const createTimeSeriesYAxis = (units: string,
decimals,
settings,
option: {
mainType: 'yAxis',
show: settings.show,
type: 'value',
position: settings.position,
@ -1011,75 +1040,87 @@ export const createTimeSeriesYAxis = (units: string,
};
};
export const createTimeSeriesXAxisOption = (settings: TimeSeriesChartXAxisSettings,
min: number, max: number,
datePipe: DatePipe,
darkMode: boolean): XAXisOption => {
export const createTimeSeriesXAxis = (id: string,
settings: TimeSeriesChartXAxisSettings,
min: number, max: number,
datePipe: DatePipe,
darkMode: boolean): TimeSeriesChartXAxis => {
const xAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont,
settings.tickLabelColor, darkMode, 'axis.tickLabel');
const xAxisNameStyle = createChartTextStyle(settings.labelFont,
settings.labelColor, darkMode, 'axis.label');
const ticksFormat = mergeDeep({}, defaultXAxisTicksFormat, settings.ticksFormat);
return {
show: settings.show,
type: 'time',
scale: true,
position: settings.position,
name: settings.label,
nameLocation: 'middle',
nameTextStyle: {
color: xAxisNameStyle.color,
fontStyle: xAxisNameStyle.fontStyle,
fontWeight: xAxisNameStyle.fontWeight,
fontFamily: xAxisNameStyle.fontFamily,
fontSize: xAxisNameStyle.fontSize
},
axisTick: {
show: settings.showTicks,
lineStyle: {
color: prepareChartThemeColor(settings.ticksColor, darkMode, 'axis.ticks')
}
},
axisLabel: {
show: settings.showTickLabels,
color: xAxisTickLabelStyle.color,
fontStyle: xAxisTickLabelStyle.fontStyle,
fontWeight: xAxisTickLabelStyle.fontWeight,
fontFamily: xAxisTickLabelStyle.fontFamily,
fontSize: xAxisTickLabelStyle.fontSize,
hideOverlap: true,
/** Min/Max time label always visible **/
/* alignMinLabel: 'left',
alignMaxLabel: 'right',
showMinLabel: true,
showMaxLabel: true, */
formatter: (value: number, _index: number, extra: {level: number}) => {
const unit = tsToFormatTimeUnit(value);
const format = ticksFormat[unit];
const formatted = datePipe.transform(value, format);
if (extra.level > 0) {
return `{primary|${formatted}}`;
} else {
return formatted;
id,
settings,
option: {
mainType: 'xAxis',
show: settings.show,
type: 'time',
scale: true,
position: settings.position,
id,
name: settings.label,
nameLocation: 'middle',
nameTextStyle: {
color: xAxisNameStyle.color,
fontStyle: xAxisNameStyle.fontStyle,
fontWeight: xAxisNameStyle.fontWeight,
fontFamily: xAxisNameStyle.fontFamily,
fontSize: xAxisNameStyle.fontSize
},
axisPointer: {
shadowStyle: {
color: id === 'main' ? 'rgba(210,219,238,0.2)' : 'rgba(150,150,150,0.1)'
}
}
},
axisLine: {
show: settings.showLine,
onZero: false,
lineStyle: {
color: prepareChartThemeColor(settings.lineColor, darkMode, 'axis.line')
}
},
splitLine: {
show: settings.showSplitLines,
lineStyle: {
color: prepareChartThemeColor(settings.splitLinesColor, darkMode, 'axis.splitLine')
}
},
min,
max,
bandWidthCalculator: timeAxisBandWidthCalculator
},
axisTick: {
show: settings.showTicks,
lineStyle: {
color: prepareChartThemeColor(settings.ticksColor, darkMode, 'axis.ticks')
}
},
axisLabel: {
show: settings.showTickLabels,
color: xAxisTickLabelStyle.color,
fontStyle: xAxisTickLabelStyle.fontStyle,
fontWeight: xAxisTickLabelStyle.fontWeight,
fontFamily: xAxisTickLabelStyle.fontFamily,
fontSize: xAxisTickLabelStyle.fontSize,
hideOverlap: true,
/** Min/Max time label always visible **/
/* alignMinLabel: 'left',
alignMaxLabel: 'right',
showMinLabel: true,
showMaxLabel: true, */
formatter: (value: number, _index: number, extra: {level: number}) => {
const unit = tsToFormatTimeUnit(value);
const format = ticksFormat[unit];
const formatted = datePipe.transform(value, format);
if (extra.level > 0) {
return `{primary|${formatted}}`;
} else {
return formatted;
}
}
},
axisLine: {
show: settings.showLine,
onZero: false,
lineStyle: {
color: prepareChartThemeColor(settings.lineColor, darkMode, 'axis.line')
}
},
splitLine: {
show: settings.showSplitLines,
lineStyle: {
color: prepareChartThemeColor(settings.splitLinesColor, darkMode, 'axis.splitLine')
}
},
min,
max,
bandWidthCalculator: timeAxisBandWidthCalculator
}
};
};
@ -1098,6 +1139,13 @@ export const createTimeSeriesVisualMapOption = (settings: TimeSeriesChartVisualM
} : undefined
});
export const updateXAxisTimeWindow = (option: XAXisOption,
timeWindow: WidgetTimewindow) => {
option.min = timeWindow.minTime;
option.max = timeWindow.maxTime;
(option as any).tbTimeWindow = timeWindow;
};
export const generateChartData = (dataItems: TimeSeriesChartDataItem[],
thresholdItems: TimeSeriesChartThresholdItem[],
stack: boolean,
@ -1262,6 +1310,7 @@ const generateChartSeries = (dataItems: TimeSeriesChartDataItem[],
};
export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChartSettings,
xAxisList: TimeSeriesChartXAxis[],
yAxisList: TimeSeriesChartYAxis[],
dataItems: TimeSeriesChartDataItem[],
darkMode: boolean): EChartsOption => {
@ -1278,12 +1327,14 @@ export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChart
}
}
if (Array.isArray(options.xAxis)) {
for (const xAxis of options.xAxis) {
xAxis.nameTextStyle.color = prepareChartThemeColor(settings.xAxis.labelColor, darkMode, 'axis.label');
xAxis.axisLabel.color = prepareChartThemeColor(settings.xAxis.tickLabelColor, darkMode, 'axis.tickLabel');
xAxis.axisLine.lineStyle.color = prepareChartThemeColor(settings.xAxis.lineColor, darkMode, 'axis.line');
xAxis.axisTick.lineStyle.color = prepareChartThemeColor(settings.xAxis.ticksColor, darkMode, 'axis.ticks');
xAxis.splitLine.lineStyle.color = prepareChartThemeColor(settings.xAxis.splitLinesColor, darkMode, 'axis.splitLine');
for (let i = 0; i < options.xAxis.length; i++) {
const xAxis = options.xAxis[i];
const xAxisSettings = xAxisList[i].settings;
xAxis.nameTextStyle.color = prepareChartThemeColor(xAxisSettings.labelColor, darkMode, 'axis.label');
xAxis.axisLabel.color = prepareChartThemeColor(xAxisSettings.tickLabelColor, darkMode, 'axis.tickLabel');
xAxis.axisLine.lineStyle.color = prepareChartThemeColor(xAxisSettings.lineColor, darkMode, 'axis.line');
xAxis.axisTick.lineStyle.color = prepareChartThemeColor(xAxisSettings.ticksColor, darkMode, 'axis.ticks');
xAxis.splitLine.lineStyle.color = prepareChartThemeColor(xAxisSettings.splitLinesColor, darkMode, 'axis.splitLine');
}
}
for (const item of dataItems) {
@ -1322,6 +1373,7 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem,
seriesOption = {
id: item.id,
dataGroupId: item.id,
xAxisIndex: item.xAxisIndex,
yAxisIndex: item.yAxisIndex,
name: item.dataKey.label,
color: seriesColor,

142
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts

@ -16,16 +16,16 @@
import { WidgetContext } from '@home/models/widget-component.models';
import {
AxisPosition,
calculateThresholdsOffset,
createTimeSeriesVisualMapOption,
createTimeSeriesXAxisOption,
createTimeSeriesXAxis,
createTimeSeriesYAxis,
defaultTimeSeriesChartYAxisSettings,
generateChartData,
LineSeriesStepType,
parseThresholdData,
SeriesLabelPosition,
TimeSeriesChartAxis,
TimeSeriesChartDataItem,
timeSeriesChartDefaultSettings,
timeSeriesChartKeyDefaultSettings,
@ -38,16 +38,17 @@ import {
TimeSeriesChartThresholdItem,
TimeSeriesChartThresholdType,
TimeSeriesChartType,
TimeSeriesChartXAxis,
TimeSeriesChartYAxis,
TimeSeriesChartYAxisId,
TimeSeriesChartYAxisSettings,
updateDarkMode
updateDarkMode,
updateXAxisTimeWindow
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { ResizeObserver } from '@juggle/resize-observer';
import {
adjustTimeAxisExtentToData,
calculateXAxisHeight,
calculateYAxisWidth,
calculateAxisSize,
createTooltipValueFormatFunction,
ECharts,
echartsModule,
@ -58,8 +59,7 @@ import {
EChartsTooltipValueFormatFunction,
getAxisExtent,
getFocusedSeriesIndex,
measureXAxisNameHeight,
measureYAxisNameWidth,
measureAxisNameSize,
toNamedData
} from '@home/components/widget/lib/chart/echarts-widget.models';
import { DateFormatProcessor } from '@shared/models/widget-settings.models';
@ -120,6 +120,10 @@ export class TbTimeSeriesChart {
private readonly settings: TimeSeriesChartSettings;
private readonly comparisonEnabled: boolean;
private readonly stackMode: boolean;
private xAxisList: TimeSeriesChartXAxis[] = [];
private yAxisList: TimeSeriesChartYAxis[] = [];
private dataItems: TimeSeriesChartDataItem[] = [];
private thresholdItems: TimeSeriesChartThresholdItem[] = [];
@ -163,6 +167,8 @@ export class TbTimeSeriesChart {
this.settings = mergeDeep({} as TimeSeriesChartSettings,
timeSeriesChartDefaultSettings,
this.inputSettings as TimeSeriesChartSettings);
this.comparisonEnabled = !!this.ctx.defaultSubscription.comparisonEnabled;
this.stackMode = !this.comparisonEnabled && this.settings.stack;
if (this.settings.states && this.settings.states.length) {
this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.dashboard.utils, this.settings.states);
this.tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter;
@ -170,6 +176,7 @@ export class TbTimeSeriesChart {
const $dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page');
const dashboardPageElement = $dashboardPageElement.length ? $($dashboardPageElement[$dashboardPageElement.length-1]) : null;
this.darkMode = this.settings.darkMode || dashboardPageElement?.hasClass('dark');
this.setupXAxes();
this.setupYAxes();
this.setupData();
this.setupThresholds();
@ -216,14 +223,16 @@ export class TbTimeSeriesChart {
}
this.onResize();
if (this.timeSeriesChart) {
this.timeSeriesChartOptions.xAxis[0].min = this.ctx.defaultSubscription.timeWindow.minTime;
this.timeSeriesChartOptions.xAxis[0].max = this.ctx.defaultSubscription.timeWindow.maxTime;
this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow;
updateXAxisTimeWindow(this.xAxisList[0].option, this.ctx.defaultSubscription.timeWindow);
if (this.noAggregation) {
this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'line';
} else {
this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'shadow';
}
if (this.comparisonEnabled) {
updateXAxisTimeWindow(this.xAxisList[1].option, this.ctx.defaultSubscription.comparisonTimeWindow);
}
this.timeSeriesChartOptions.xAxis = this.xAxisList.map(axis => axis.option);
if (this.hasVisualMap) {
(this.timeSeriesChartOptions.visualMap as PiecewiseVisualMapOption).selected = this.visualMapSelectedRanges;
}
@ -300,7 +309,7 @@ export class TbTimeSeriesChart {
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
mergeList.push('yAxis');
}
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.settings.stack ? {notMerge: true} : {replaceMerge: mergeList});
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.stackMode ? {notMerge: true} : {replaceMerge: mergeList});
this.updateAxes();
dataKey.hidden = !enable;
if (enable) {
@ -343,7 +352,7 @@ export class TbTimeSeriesChart {
this.darkMode = darkMode;
if (this.timeSeriesChart) {
this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions,
this.settings, this.yAxisList, this.dataItems, darkMode);
this.settings, this.xAxisList, this.yAxisList, this.dataItems, darkMode);
this.timeSeriesChart.setOption(this.timeSeriesChartOptions);
}
}
@ -392,12 +401,16 @@ export class TbTimeSeriesChart {
if (!Object.keys(this.settings.yAxes).includes(yAxisId)) {
yAxisId = 'default';
}
const comparisonItem = this.comparisonEnabled && dataKey.isAdditional;
const xAxisIndex = comparisonItem ? 1 : 0;
this.dataItems.push({
id: this.nextComponentId(),
units,
decimals,
xAxisIndex,
yAxisId,
yAxisIndex: this.getYAxisIndex(yAxisId),
comparisonItem,
datasource,
dataKey,
data: namedData,
@ -488,6 +501,18 @@ export class TbTimeSeriesChart {
this.subscribeForEntityThresholds(thresholdDatasources);
}
private setupXAxes(): void {
const mainXAxis = createTimeSeriesXAxis('main', this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime,
this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.darkMode);
this.xAxisList.push(mainXAxis);
if (this.comparisonEnabled) {
const comparisonXAxis = createTimeSeriesXAxis('comparison', this.settings.comparisonXAxis,
this.ctx.defaultSubscription.comparisonTimeWindow.minTime, this.ctx.defaultSubscription.comparisonTimeWindow.maxTime,
this.ctx.date, this.darkMode);
this.xAxisList.push(comparisonXAxis);
}
}
private setupYAxes(): void {
const yAxisSettingsList = Object.values(this.settings.yAxes);
yAxisSettingsList.sort((a1, a2) => a1.order - a2.order);
@ -592,10 +617,7 @@ export class TbTimeSeriesChart {
right: this.settings.dataZoom ? 5 : 0,
bottom: this.minBottomOffset()
}],
xAxis: [
createTimeSeriesXAxisOption(this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime,
this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.darkMode)
],
xAxis: this.xAxisList.map(axis => axis.option),
yAxis: this.yAxisList.map(axis => axis.option),
dataZoom: [
{
@ -655,10 +677,10 @@ export class TbTimeSeriesChart {
private updateSeries(): void {
this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems,
this.settings.stack,
this.stackMode,
this.noAggregation,
this.barRenderSharedContext, this.darkMode);
if (this.stateData) {
if (this.stateData && !this.comparisonEnabled) {
adjustTimeAxisExtentToData(this.timeSeriesChartOptions.xAxis[0], this.dataItems,
this.ctx.defaultSubscription.timeWindow.minTime,
this.ctx.defaultSubscription.timeWindow.maxTime);
@ -667,37 +689,26 @@ export class TbTimeSeriesChart {
private updateAxes(lazy = true) {
const leftAxisList = this.yAxisList.filter(axis => axis.option.position === 'left');
let res = this.updateYAxisOffset(leftAxisList);
let res = this.updateAxisOffset(leftAxisList);
let leftOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0);
let changed = res.changed;
const rightAxisList = this.yAxisList.filter(axis => axis.option.position === 'right');
res = this.updateYAxisOffset(rightAxisList);
res = this.updateAxisOffset(rightAxisList);
let rightOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0);
changed = changed || res.changed;
let bottomOffset = this.minBottomOffset();
const minTopOffset = this.minTopOffset();
let topOffset = minTopOffset;
if (this.timeSeriesChartOptions.xAxis[0].show) {
const xAxisHeight = calculateXAxisHeight(this.timeSeriesChart);
if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) {
bottomOffset += xAxisHeight;
} else {
topOffset = Math.max(minTopOffset, xAxisHeight);
}
if (this.settings.xAxis.label) {
const nameHeight = measureXAxisNameHeight(this.timeSeriesChart, this.timeSeriesChartOptions.xAxis[0].name);
if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) {
bottomOffset += nameHeight;
} else {
topOffset = Math.max(minTopOffset, xAxisHeight + nameHeight);
}
const nameGap = xAxisHeight;
if (this.timeSeriesChartOptions.xAxis[0].nameGap !== nameGap) {
this.timeSeriesChartOptions.xAxis[0].nameGap = nameGap;
changed = true;
}
}
}
const topAxisList = this.xAxisList.filter(axis => axis.option.position === 'top');
res = this.updateAxisOffset(topAxisList);
const topOffset = Math.max(res.offset, minTopOffset);
changed = changed || res.changed;
const bottomAxisList = this.xAxisList.filter(axis => axis.option.position === 'bottom');
res = this.updateAxisOffset(bottomAxisList);
bottomOffset += res.offset;
changed = changed || res.changed;
const thresholdsOffset = calculateThresholdsOffset(this.timeSeriesChart, this.thresholdItems, this.yAxisList);
leftOffset = Math.max(leftOffset, thresholdsOffset[0]);
@ -715,6 +726,7 @@ export class TbTimeSeriesChart {
}
if (changed) {
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
this.timeSeriesChartOptions.xAxis = this.xAxisList.map(axis => axis.option);
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis', 'xAxis', 'grid'], lazyUpdate: lazy});
}
if (this.yAxisList.length) {
@ -730,45 +742,45 @@ export class TbTimeSeriesChart {
}
}
private updateYAxisOffset(axisList: TimeSeriesChartYAxis[]): {offset: number; changed: boolean} {
private updateAxisOffset(axisList: TimeSeriesChartAxis[]): {offset: number; changed: boolean} {
const result = {offset: 0, changed: false};
let width = 0;
for (const yAxis of axisList) {
const newWidth = calculateYAxisWidth(this.timeSeriesChart, yAxis.id);
if (width && newWidth) {
let size = 0;
for (const axis of axisList) {
const newSize = calculateAxisSize(this.timeSeriesChart, axis.option.mainType, axis.id);
if (size && newSize) {
result.offset += 5;
}
width = newWidth;
const showLine = !!width && yAxis.settings.showLine;
if (yAxis.option.axisLine.show !== showLine) {
yAxis.option.axisLine.show = showLine;
size = newSize;
const showLine = !!size && axis.settings.showLine;
if (axis.option.axisLine.show !== showLine) {
axis.option.axisLine.show = showLine;
result.changed = true;
}
if (yAxis.option.offset !== result.offset) {
yAxis.option.offset = result.offset;
if (axis.option.offset !== result.offset) {
axis.option.offset = result.offset;
result.changed = true;
}
if (yAxis.settings.label) {
if (!width) {
if (yAxis.option.name) {
yAxis.option.name = null;
if (axis.settings.label) {
if (!size) {
if (axis.option.name) {
axis.option.name = null;
result.changed = true;
}
} else {
if (!yAxis.option.name) {
yAxis.option.name = yAxis.settings.label;
if (!axis.option.name) {
axis.option.name = axis.settings.label;
result.changed = true;
}
const nameGap = width;
if (yAxis.option.nameGap !== nameGap) {
yAxis.option.nameGap = nameGap;
const nameGap = size;
if (axis.option.nameGap !== nameGap) {
axis.option.nameGap = nameGap;
result.changed = true;
}
const nameWidth = measureYAxisNameWidth(this.timeSeriesChart, yAxis.id, yAxis.settings.label);
result.offset += nameWidth;
const nameSize = measureAxisNameSize(this.timeSeriesChart, axis.option.mainType, axis.id, axis.settings.label);
result.offset += nameSize;
}
}
result.offset += width;
result.offset += size;
}
return result;
}

12
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts

@ -18,11 +18,10 @@
/// <reference path="../../../../../../../src/typings/jquery.flot.typings.d.ts" />
import {
DataKey,
DataKey, DataKeySettingsWithComparison,
Datasource,
DatasourceData,
FormattedData,
JsonSettingsSchema,
LegendConfig
} from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -219,13 +218,7 @@ export interface TbFlotKeyThreshold {
color: string;
}
export interface TbFlotKeyComparisonSettings {
showValuesForComparison: boolean;
comparisonValuesLabel: string;
color: string;
}
export interface TbFlotKeySettings {
export interface TbFlotKeySettings extends DataKeySettingsWithComparison {
excludeFromStacking: boolean;
hideDataByDefault: boolean;
disableDataHiding: boolean;
@ -249,7 +242,6 @@ export interface TbFlotKeySettings {
axisPosition: TbFlotYAxisPosition;
axisTicksFormatter: string;
thresholds: TbFlotKeyThreshold[];
comparisonSettings: TbFlotKeyComparisonSettings;
}
export interface TbFlotLatestKeySettings {

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts

@ -94,6 +94,11 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
lineSettings: [seriesSettings.lineSettings, []],
barSettings: [seriesSettings.barSettings, []],
tooltipValueFormatter: [seriesSettings.tooltipValueFormatter, []],
comparisonSettings: this.fb.group({
showValuesForComparison: [seriesSettings.comparisonSettings?.showValuesForComparison, []],
comparisonValuesLabel: [seriesSettings.comparisonSettings?.comparisonValuesLabel, []],
color: [seriesSettings.comparisonSettings?.color, []]
})
});
}

16
ui-ngx/src/app/shared/models/widget.models.ts

@ -21,7 +21,6 @@ import { AggregationType, ComparisonDuration, Timewindow } from '@shared/models/
import { EntityType } from '@shared/models/entity-type.models';
import { DataKeyType } from './telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import * as moment_ from 'moment';
import {
AlarmFilter,
AlarmFilterConfig,
@ -710,10 +709,23 @@ export const defaultWidgetAction = (setEntityId = true): WidgetAction => ({
export interface WidgetComparisonSettings {
comparisonEnabled?: boolean;
timeForComparison?: moment_.unitOfTime.DurationConstructor;
timeForComparison?: ComparisonDuration;
comparisonCustomIntervalValue?: number;
}
export interface DataKeyComparisonSettings {
showValuesForComparison: boolean;
comparisonValuesLabel: string;
color: string;
}
export interface DataKeySettingsWithComparison {
comparisonSettings?: DataKeyComparisonSettings;
}
export const isDataKeySettingsWithComparison = (settings: any): settings is DataKeySettingsWithComparison =>
'comparisonSettings' in settings;
export interface WidgetSettings {
[key: string]: any;
}

Loading…
Cancel
Save