Chantsova Ekaterina df784db8a1 Refactoring
2020-10-12 19:31:42 +03:00

1468 lines
51 KiB
TypeScript

///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { WidgetContext } from '@home/models/widget-component.models';
import {
createLabelFromDatasource,
deepClone,
insertVariable,
isDefined,
isDefinedAndNotNull,
isEqual,
isNumber,
isUndefined
} from '@app/core/utils';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import {
DataKey,
Datasource,
DatasourceData,
DatasourceType,
JsonSettingsSchema,
widgetType
} from '@app/shared/models/widget.models';
import {
ChartType,
flotDatakeySettingsSchema,
flotPieDatakeySettingsSchema,
flotPieSettingsSchema,
flotSettingsSchema,
TbFlotAxisOptions,
TbFlotHoverInfo,
TbFlotKeySettings,
TbFlotPlotAxis,
TbFlotPlotDataSeries,
TbFlotPlotItem,
TbFlotSeries,
TbFlotSeriesHoverInfo,
TbFlotSettings,
TbFlotThresholdKeySettings,
TbFlotThresholdMarking,
TbFlotTicksFormatterFunction,
TooltipValueFormatFunction
} from './flot-widget.models';
import * as moment_ from 'moment';
import * as tinycolor_ from 'tinycolor2';
import { AggregationType } from '@shared/models/time/time.models';
import { CancelAnimationFrame } from '@core/services/raf.service';
import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import Timeout = NodeJS.Timeout;
const tinycolor = tinycolor_;
const moment = moment_;
const flotPieSettingsSchemaValue = flotPieSettingsSchema;
const flotPieDatakeySettingsSchemaValue = flotPieDatakeySettingsSchema;
export class TbFlot {
private readonly utils: UtilsService;
private settings: TbFlotSettings;
private readonly tooltip: JQuery<any>;
private readonly yAxisTickFormatter: TbFlotTicksFormatterFunction;
private readonly yaxis: TbFlotAxisOptions;
private readonly xaxis: TbFlotAxisOptions;
private yaxes: Array<TbFlotAxisOptions>;
private readonly options: JQueryPlotOptions;
private subscription: IWidgetSubscription;
private $element: JQuery<any>;
private readonly trackUnits: string;
private readonly trackDecimals: number;
private readonly tooltipIndividual: boolean;
private readonly tooltipCumulative: boolean;
private readonly hideZeros: boolean;
private readonly defaultBarWidth: number;
private thresholdsSourcesSubscription: IWidgetSubscription;
private predefinedThresholds: TbFlotThresholdMarking[];
private labelPatternsSourcesSubscription: IWidgetSubscription;
private labelPatternsSourcesData: DatasourceData[];
private plotInited = false;
private plot: JQueryPlot;
private createPlotTimeoutHandle: Timeout;
private updateTimeoutHandle: Timeout;
private resizeTimeoutHandle: Timeout;
private mouseEventsEnabled: boolean;
private isMouseInteraction = false;
private flotHoverHandler = this.onFlotHover.bind(this);
private flotSelectHandler = this.onFlotSelect.bind(this);
private dblclickHandler = this.onFlotDblClick.bind(this);
private mousedownHandler = this.onFlotMouseDown.bind(this);
private mouseupHandler = this.onFlotMouseUp.bind(this);
private mouseleaveHandler = this.onFlotMouseLeave.bind(this);
private flotClickHandler = this.onFlotClick.bind(this);
private readonly animatedPie: boolean;
private pieDataAnimationDuration: number;
private pieData: DatasourceData[];
private pieRenderedData: any[];
private pieTargetData: any[];
private pieAnimationStartTime: number;
private pieAnimationLastTime: number;
private pieAnimationCaf: CancelAnimationFrame;
static pieSettingsSchema(): JsonSettingsSchema {
return flotPieSettingsSchemaValue;
}
static pieDatakeySettingsSchema(): JsonSettingsSchema {
return flotPieDatakeySettingsSchemaValue;
}
static settingsSchema(chartType: ChartType): JsonSettingsSchema {
return flotSettingsSchema(chartType);
}
static datakeySettingsSchema(defaultShowLines: boolean, chartType: ChartType): JsonSettingsSchema {
return flotDatakeySettingsSchema(defaultShowLines, chartType);
}
constructor(private ctx: WidgetContext, private readonly chartType: ChartType) {
this.chartType = this.chartType || 'line';
this.settings = ctx.settings as TbFlotSettings;
this.utils = this.ctx.$injector.get(UtilsService);
this.tooltip = $('#flot-series-tooltip');
if (this.tooltip.length === 0) {
this.tooltip = this.createTooltipElement();
}
this.trackDecimals = ctx.decimals;
this.trackUnits = ctx.units;
this.tooltipIndividual = this.chartType === 'pie' || (isDefined(this.settings.tooltipIndividual)
? this.settings.tooltipIndividual : false);
this.tooltipCumulative = isDefined(this.settings.tooltipCumulative) ? this.settings.tooltipCumulative : false;
this.hideZeros = isDefined(this.settings.hideZeros) ? this.settings.hideZeros : false;
const font = {
color: this.settings.fontColor || '#545454',
size: this.settings.fontSize || 10,
family: 'Roboto'
};
this.options = {
title: null,
subtitile: null,
shadowSize: isDefined(this.settings.shadowSize) ? this.settings.shadowSize : 4,
HtmlText: false,
grid: {
hoverable: true,
mouseActiveRadius: 10,
autoHighlight: this.tooltipIndividual === true,
markings: []
},
selection : { mode : ctx.isMobile ? null : 'x' },
legend : {
show: false
}
};
if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
this.options.xaxes = [];
this.xaxis = {
mode: 'time',
timezone: 'browser',
font: deepClone(font),
labelFont: deepClone(font)
};
this.yaxis = {
font: deepClone(font),
labelFont: deepClone(font)
};
if (this.settings.xaxis) {
this.xaxis.font.color = this.settings.xaxis.color || this.xaxis.font.color;
this.xaxis.label = this.utils.customTranslation(this.settings.xaxis.title, this.settings.xaxis.title) || null;
this.xaxis.labelFont.color = this.xaxis.font.color;
this.xaxis.labelFont.size = this.xaxis.font.size + 2;
this.xaxis.labelFont.weight = 'bold';
}
this.yAxisTickFormatter = this.formatYAxisTicks.bind(this);
this.yaxis.tickFormatter = this.yAxisTickFormatter;
if (this.settings.yaxis) {
this.yaxis.font.color = this.settings.yaxis.color || this.yaxis.font.color;
this.yaxis.min = isDefined(this.settings.yaxis.min) ? this.settings.yaxis.min : null;
this.yaxis.max = isDefined(this.settings.yaxis.max) ? this.settings.yaxis.max : null;
this.yaxis.label = this.utils.customTranslation(this.settings.yaxis.title, this.settings.yaxis.title) || null;
this.yaxis.labelFont.color = this.yaxis.font.color;
this.yaxis.labelFont.size = this.yaxis.font.size + 2;
this.yaxis.labelFont.weight = 'bold';
if (isNumber(this.settings.yaxis.tickSize)) {
this.yaxis.tickSize = this.settings.yaxis.tickSize;
} else {
this.yaxis.tickSize = null;
}
if (isNumber(this.settings.yaxis.tickDecimals)) {
this.yaxis.tickDecimals = this.settings.yaxis.tickDecimals;
} else {
this.yaxis.tickDecimals = null;
}
if (this.settings.yaxis.ticksFormatter && this.settings.yaxis.ticksFormatter.length) {
try {
this.yaxis.ticksFormatterFunction = new Function('value',
this.settings.yaxis.ticksFormatter) as TbFlotTicksFormatterFunction;
} catch (e) {
this.yaxis.ticksFormatterFunction = null;
}
}
}
this.options.grid.borderWidth = 1;
this.options.grid.color = this.settings.fontColor || '#545454';
if (this.settings.grid) {
this.options.grid.color = this.settings.grid.color || '#545454';
this.options.grid.backgroundColor = this.settings.grid.backgroundColor || null;
this.options.grid.tickColor = this.settings.grid.tickColor || '#DDDDDD';
this. options.grid.borderWidth = isDefined(this.settings.grid.outlineWidth) ?
this.settings.grid.outlineWidth : 1;
if (this.settings.grid.verticalLines === false) {
this.xaxis.tickLength = 0;
}
if (this.settings.grid.horizontalLines === false) {
this.yaxis.tickLength = 0;
}
if (isDefined(this.settings.grid.margin)) {
this.options.grid.margin = this.settings.grid.margin;
}
if (isDefined(this.settings.grid.minBorderMargin)) {
this.options.grid.minBorderMargin = this.settings.grid.minBorderMargin;
}
}
this.options.xaxes[0] = deepClone(this.xaxis);
if (this.settings.xaxis && this.settings.xaxis.showLabels === false) {
this.options.xaxes[0].tickFormatter = () => {
return '';
};
}
if (this.settings.comparisonEnabled) {
const xaxis = deepClone(this.xaxis);
xaxis.position = 'top';
if (this.settings.xaxisSecond) {
if (this.settings.xaxisSecond.showLabels === false) {
xaxis.tickFormatter = () => {
return '';
};
}
xaxis.label = this.utils.customTranslation(this.settings.xaxisSecond.title, this.settings.xaxisSecond.title) || null;
xaxis.position = this.settings.xaxisSecond.axisPosition;
}
xaxis.tickLength = 0;
this.options.xaxes.push(xaxis);
this.options.series = {
stack: false
};
} else {
this.options.series = {
stack: this.settings.stack === true
};
}
this.options.crosshair = {
mode: 'x'
};
if (this.chartType === 'line' && this.settings.smoothLines) {
this.options.series.curvedLines = {
active: true,
monotonicFit: true
};
}
if (this.chartType === 'line' && isFinite(this.settings.thresholdsLineWidth)) {
this.options.grid.markingsLineWidth = this.settings.thresholdsLineWidth;
}
if (this.chartType === 'bar') {
this.options.series.lines = {
show: false,
fill: false,
steps: false
};
this.options.series.bars = {
show: true,
lineWidth: 0,
fill: 0.9,
align: this.settings.barAlignment || 'left'
};
this.defaultBarWidth = this.settings.defaultBarWidth || 600;
}
if (this.chartType === 'state') {
this.options.series.lines = {
steps: true,
show: true
};
}
} else if (this.chartType === 'pie') {
this.options.series = {
pie: {
show: true,
label: {
show: this.settings.showLabels === true
},
radius: this.settings.radius || 1,
innerRadius: this.settings.innerRadius || 0,
stroke: {
color: '#fff',
width: 0
},
tilt: this.settings.tilt || 1,
shadow: {
left: 5,
top: 15,
alpha: 0.02
}
}
};
this.options.grid.clickable = true;
if (this.settings.stroke) {
this.options.series.pie.stroke.color = this.settings.stroke.color || '#fff';
this.options.series.pie.stroke.width = this.settings.stroke.width || 0;
}
if (this.options.series.pie.label.show) {
this.options.series.pie.label.formatter = (label, series) => {
return `<div class='pie-label'>${series.dataKey.label}<br/>${Math.round(series.percent)}%</div>`;
};
this.options.series.pie.label.radius = 3 / 4;
this.options.series.pie.label.background = {
opacity: 0.8
};
}
// Experimental
this.animatedPie = this.settings.animatedPie === true;
}
if (this.ctx.defaultSubscription) {
this.init(this.ctx.$container, this.ctx.defaultSubscription);
}
}
private init($element: JQuery<any>, subscription: IWidgetSubscription) {
this.subscription = subscription;
this.$element = $element;
const colors: string[] = [];
this.yaxes = [];
const yaxesMap: {[units: string]: TbFlotAxisOptions} = {};
const predefinedThresholds: TbFlotThresholdMarking[] = [];
const thresholdsDatasources: Datasource[] = [];
if (this.settings.customLegendEnabled && this.settings.dataKeysListForLabels?.length) {
this.labelPatternsSourcesData = [];
const labelPatternsDatasources: Datasource[] = [];
this.settings.dataKeysListForLabels.forEach((item) => {
item.settings = {};
});
subscription.datasources.forEach((item) => {
let datasource: Datasource = {
type: item.type,
entityType: item.entityType,
entityId: item.entityId,
dataKeys: this.settings.dataKeysListForLabels
};
labelPatternsDatasources.push(datasource);
});
this.subscribeForLabelPatternsSources(labelPatternsDatasources);
}
let tooltipValueFormatFunction: TooltipValueFormatFunction = null;
if (this.settings.tooltipValueFormatter && this.settings.tooltipValueFormatter.length) {
try {
tooltipValueFormatFunction = new Function('value', this.settings.tooltipValueFormatter) as TooltipValueFormatFunction;
} catch (e) {
tooltipValueFormatFunction = null;
}
}
for (let i = 0; i < this.subscription.data.length; i++) {
const series = this.subscription.data[i] as TbFlotSeries;
colors.push(series.dataKey.color);
const keySettings = series.dataKey.settings;
series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction;
if (keySettings.tooltipValueFormatter && keySettings.tooltipValueFormatter.length) {
try {
series.dataKey.tooltipValueFormatFunction = new Function('value',
keySettings.tooltipValueFormatter) as TooltipValueFormatFunction;
} catch (e) {
series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction;
}
}
series.lines = {
fill: keySettings.fillLines === true
};
if (this.settings.stack && !this.settings.comparisonEnabled) {
series.stack = !keySettings.excludeFromStacking;
} else {
series.stack = false;
}
if (this.chartType === 'line' || this.chartType === 'state') {
series.lines.show = keySettings.showLines !== false;
} else {
series.lines.show = keySettings.showLines === true;
}
if (isDefined(keySettings.lineWidth) && keySettings.lineWidth !== null) {
series.lines.lineWidth = keySettings.lineWidth;
}
series.points = {
show: false,
radius: 8
};
if (keySettings.showPoints === true) {
series.points.show = true;
series.points.lineWidth = isDefined(keySettings.showPointsLineWidth) ? keySettings.showPointsLineWidth : 5;
series.points.radius = isDefined(keySettings.showPointsRadius) ? keySettings.showPointsRadius : 3;
series.points.symbol = isDefined(keySettings.showPointShape) ? keySettings.showPointShape : 'circle';
if (series.points.symbol === 'custom' && keySettings.pointShapeFormatter) {
try {
series.points.symbol = new Function('ctx, x, y, radius, shadow', keySettings.pointShapeFormatter);
} catch (e) {
series.points.symbol = 'circle';
}
}
}
if (this.chartType === 'line' && this.settings.smoothLines && !series.points.show) {
series.curvedLines = {
apply: true
};
}
const lineColor = tinycolor(series.dataKey.color);
lineColor.setAlpha(.75);
series.highlightColor = lineColor.toRgbString();
if (series.datasource.isAdditional) {
series.xaxisIndex = 1;
series.xaxis = 2;
} else {
series.xaxisIndex = 0;
series.xaxis = 1;
}
if (this.yaxis) {
const units = series.dataKey.units && series.dataKey.units.length ? series.dataKey.units : this.trackUnits;
let yaxis: TbFlotAxisOptions;
if (keySettings.showSeparateAxis) {
yaxis = this.createYAxis(keySettings, units);
this.yaxes.push(yaxis);
} else {
yaxis = yaxesMap[units];
if (!yaxis) {
yaxis = this.createYAxis(keySettings, units);
yaxesMap[units] = yaxis;
this.yaxes.push(yaxis);
}
}
series.yaxisIndex = this.yaxes.indexOf(yaxis);
series.yaxis = series.yaxisIndex + 1;
yaxis.keysInfo[i] = {hidden: false};
yaxis.show = true;
if (keySettings.thresholds && keySettings.thresholds.length) {
for (const threshold of keySettings.thresholds) {
if (threshold.thresholdValueSource === 'predefinedValue' && isFinite(threshold.thresholdValue)) {
const colorIndex = this.subscription.data.length + predefinedThresholds.length;
this.generateThreshold(predefinedThresholds, series.yaxis, threshold.lineWidth,
threshold.color, colorIndex, threshold.thresholdValue);
} else if (threshold.thresholdEntityAlias && threshold.thresholdAttribute) {
const entityAliasId = this.ctx.aliasController.getEntityAliasId(threshold.thresholdEntityAlias);
if (!entityAliasId) {
continue;
}
let datasource = thresholdsDatasources.filter((thresholdDatasource) => {
return thresholdDatasource.entityAliasId === entityAliasId;
})[0];
const dataKey: DataKey = {
type: DataKeyType.attribute,
name: threshold.thresholdAttribute,
label: threshold.thresholdAttribute,
settings: {
yaxis: series.yaxis,
lineWidth: threshold.lineWidth,
color: threshold.color
} as TbFlotThresholdKeySettings,
_hash: Math.random()
};
if (datasource) {
datasource.dataKeys.push(dataKey);
} else {
datasource = {
type: DatasourceType.entity,
name: threshold.thresholdEntityAlias,
aliasName: threshold.thresholdEntityAlias,
entityAliasId,
dataKeys: [ dataKey ]
};
thresholdsDatasources.push(datasource);
}
}
}
}
}
if (this.labelPatternsSourcesData?.length) {
this.substituteLabelPatterns(series, i);
}
}
this.subscribeForThresholdsAttributes(thresholdsDatasources);
this.options.grid.markings = predefinedThresholds;
this.predefinedThresholds = predefinedThresholds;
this.options.colors = colors;
this.options.yaxes = deepClone(this.yaxes);
if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
if (this.chartType === 'bar') {
if (this.subscription.timeWindowConfig.aggregation &&
this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) {
this.options.series.bars.barWidth = this.defaultBarWidth;
} else {
this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
}
}
this.options.xaxes[0].min = this.subscription.timeWindow.minTime;
this.options.xaxes[0].max = this.subscription.timeWindow.maxTime;
if (this.settings.comparisonEnabled) {
this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime;
this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime;
}
}
this.checkMouseEvents();
if (this.plot) {
this.plot.destroy();
}
if (this.chartType === 'pie' && this.animatedPie) {
this.pieDataAnimationDuration = 250;
this.pieData = deepClone(this.subscription.data);
this.pieRenderedData = [];
this.pieTargetData = [];
for (let i = 0; i < this.subscription.data.length; i++) {
this.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0])
? this.subscription.data[i].data[0][1] : 0;
}
this.pieDataRendered();
}
this.plotInited = true;
this.createPlot();
}
public update() {
if (this.updateTimeoutHandle) {
clearTimeout(this.updateTimeoutHandle);
this.updateTimeoutHandle = null;
}
if (this.subscription) {
if (!this.isMouseInteraction && this.plot) {
if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
let axisVisibilityChanged = false;
if (this.yaxis) {
for (let i = 0; i < this.subscription.data.length; i++) {
const series = this.subscription.data[i] as TbFlotSeries;
const yaxisIndex = series.yaxisIndex;
if (this.yaxes[yaxisIndex].keysInfo[i].hidden !== series.dataKey.hidden) {
this.yaxes[yaxisIndex].keysInfo[i].hidden = series.dataKey.hidden;
axisVisibilityChanged = true;
}
if (this.labelPatternsSourcesData?.length) {
this.substituteLabelPatterns(series, i);
}
}
if (axisVisibilityChanged) {
this.options.yaxes.length = 0;
this.yaxes.forEach((yaxis) => {
let hidden = true;
yaxis.keysInfo.forEach((info) => {
if (info) {
hidden = hidden && info.hidden;
}
});
yaxis.hidden = hidden;
let newIndex = 1;
if (!yaxis.hidden) {
this.options.yaxes.push(yaxis);
newIndex = this.options.yaxes.length;
}
for (let k = 0; k < yaxis.keysInfo.length; k++) {
if (yaxis.keysInfo[k]) {
(this.subscription.data[k] as TbFlotSeries).yaxis = newIndex;
}
}
});
this.options.yaxis = {
show: this.options.yaxes.length ? true : false
};
}
}
this.options.xaxes[0].min = this.subscription.timeWindow.minTime;
this.options.xaxes[0].max = this.subscription.timeWindow.maxTime;
if (this.settings.comparisonEnabled) {
this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime;
this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime;
}
if (this.chartType === 'bar') {
if (this.subscription.timeWindowConfig.aggregation &&
this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) {
this.options.series.bars.barWidth = this.defaultBarWidth;
} else {
this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
}
}
if (axisVisibilityChanged) {
this.redrawPlot();
} else {
this.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime;
this.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime;
if (this.settings.comparisonEnabled) {
this.plot.getOptions().xaxes[1].min = this.subscription.comparisonTimeWindow.minTime;
this.plot.getOptions().xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime;
}
if (this.chartType === 'bar') {
if (this.subscription.timeWindowConfig.aggregation &&
this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) {
this.plot.getOptions().series.bars.barWidth = this.defaultBarWidth;
} else {
this.plot.getOptions().series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
}
}
this.updateData();
}
} else if (this.chartType === 'pie') {
if (this.animatedPie) {
this.nextPieDataAnimation(true);
} else {
this.updateData();
}
}
} else if (this.isMouseInteraction && this.plot) {
this.updateTimeoutHandle = setTimeout(this.update.bind(this), 30);
}
}
}
public resize() {
if (this.resizeTimeoutHandle) {
clearTimeout(this.resizeTimeoutHandle);
this.resizeTimeoutHandle = null;
}
if (this.plot && this.plotInited) {
const width = this.$element.width();
const height = this.$element.height();
if (width && height) {
this.plot.resize();
if (this.chartType !== 'pie') {
this.plot.setupGrid();
}
this.plot.draw();
} else {
this.resizeTimeoutHandle = setTimeout(this.resize.bind(this), 30);
}
}
}
public checkMouseEvents() {
const enabled = !this.ctx.isMobile && !this.ctx.isEdit;
if (isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled !== enabled) {
this.mouseEventsEnabled = enabled;
if (this.$element) {
if (enabled) {
this.enableMouseEvents();
} else {
this.disableMouseEvents();
}
this.redrawPlot();
}
}
}
public destroy() {
this.cleanup();
if (this.plot) {
this.plot.destroy();
this.plot = null;
this.plotInited = false;
}
}
private cleanup() {
if (this.updateTimeoutHandle) {
clearTimeout(this.updateTimeoutHandle);
this.updateTimeoutHandle = null;
}
if (this.createPlotTimeoutHandle) {
clearTimeout(this.createPlotTimeoutHandle);
this.createPlotTimeoutHandle = null;
}
if (this.resizeTimeoutHandle) {
clearTimeout(this.resizeTimeoutHandle);
this.resizeTimeoutHandle = null;
}
}
private createPlot() {
if (this.createPlotTimeoutHandle) {
clearTimeout(this.createPlotTimeoutHandle);
this.createPlotTimeoutHandle = null;
}
if (this.plotInited && !this.plot) {
const width = this.$element.width();
const height = this.$element.height();
if (width && height) {
if (this.chartType === 'pie' && this.animatedPie) {
this.plot = $.plot(this.$element, this.pieData, this.options) as JQueryPlot;
} else {
this.plot = $.plot(this.$element, this.subscription.data, this.options) as JQueryPlot;
}
} else {
this.createPlotTimeoutHandle = setTimeout(this.createPlot.bind(this), 30);
}
}
}
private updateData() {
this.plot.setData(this.subscription.data);
if (this.chartType !== 'pie') {
this.plot.setupGrid();
}
this.plot.draw();
}
private redrawPlot() {
if (this.plot && this.plotInited) {
this.plot.destroy();
this.plot = null;
this.createPlot();
}
}
private createYAxis(keySettings: TbFlotKeySettings, units: string): TbFlotAxisOptions {
const yaxis = deepClone(this.yaxis);
let tickDecimals: number;
let tickSize: number;
const label = keySettings.axisTitle && keySettings.axisTitle.length ? keySettings.axisTitle : yaxis.label;
if (isNumber(keySettings.axisTickDecimals)) {
tickDecimals = keySettings.axisTickDecimals;
} else {
tickDecimals = yaxis.tickDecimals;
}
if (isNumber(keySettings.axisTickSize)) {
tickSize = keySettings.axisTickSize;
} else {
tickSize = yaxis.tickSize;
}
const position = keySettings.axisPosition && keySettings.axisPosition.length ? keySettings.axisPosition : 'left';
const min = isDefined(keySettings.axisMin) ? keySettings.axisMin : yaxis.min;
const max = isDefined(keySettings.axisMax) ? keySettings.axisMax : yaxis.max;
yaxis.label = label;
yaxis.min = min;
yaxis.max = max;
yaxis.tickUnits = units;
yaxis.tickDecimals = tickDecimals;
yaxis.tickSize = tickSize;
if (position === 'right' && tickSize === null) {
yaxis.alignTicksWithAxis = 1;
} else {
yaxis.alignTicksWithAxis = null;
}
yaxis.position = position;
yaxis.keysInfo = [];
if (keySettings.axisTicksFormatter && keySettings.axisTicksFormatter.length) {
try {
yaxis.ticksFormatterFunction = new Function('value', keySettings.axisTicksFormatter) as TbFlotTicksFormatterFunction;
} catch (e) {
yaxis.ticksFormatterFunction = this.yaxis.ticksFormatterFunction;
}
}
return yaxis;
}
private subscribeForThresholdsAttributes(datasources: Datasource[]) {
const thresholdsSourcesSubscriptionOptions: WidgetSubscriptionOptions = {
datasources,
useDashboardTimewindow: false,
type: widgetType.latest,
callbacks: {
onDataUpdated: (subscription) => {this.thresholdsSourcesDataUpdated(subscription.data)}
}
};
this.ctx.subscriptionApi.createSubscription(thresholdsSourcesSubscriptionOptions, true).subscribe(
(subscription) => {
this.thresholdsSourcesSubscription = subscription;
}
);
}
private thresholdsSourcesDataUpdated(data: DatasourceData[]) {
const allThresholds = deepClone(this.predefinedThresholds);
data.forEach((keyData) => {
if (keyData && keyData.data && keyData.data[0]) {
const attrValue = keyData.data[0][1];
if (isFinite(attrValue)) {
const settings: TbFlotThresholdKeySettings = keyData.dataKey.settings;
const colorIndex = this.subscription.data.length + allThresholds.length;
this.generateThreshold(allThresholds, settings.yaxis, settings.lineWidth, settings.color, colorIndex, attrValue);
}
}
});
this.options.grid.markings = allThresholds;
this.redrawPlot();
}
private generateThreshold(existingThresholds: TbFlotThresholdMarking[], yaxis: number, lineWidth: number,
color: string, defaultColorIndex: number, thresholdValue: number) {
const marking: TbFlotThresholdMarking = {};
let markingYAxis;
if (yaxis !== 1) {
markingYAxis = 'y' + yaxis + 'axis';
} else {
markingYAxis = 'yaxis';
}
if (isFinite(lineWidth)) {
marking.lineWidth = lineWidth;
}
if (isDefined(color)) {
marking.color = color;
} else {
marking.color = this.utils.getMaterialColor(defaultColorIndex);
}
marking[markingYAxis] = {
from: thresholdValue,
to: thresholdValue
};
const similarMarkings = existingThresholds.filter((existingMarking) => {
return isEqual(existingMarking[markingYAxis], marking[markingYAxis]);
});
if (!similarMarkings.length) {
existingThresholds.push(marking);
}
}
private subscribeForLabelPatternsSources(datasources: Datasource[]) {
const labelPatternsSourcesSubscriptionOptions: WidgetSubscriptionOptions = {
datasources,
useDashboardTimewindow: false,
type: widgetType.latest,
callbacks: {
onDataUpdated: (subscription) => {
this.labelPatternsParamsDataUpdated(subscription.data)
}
}
};
this.ctx.subscriptionApi.createSubscription(labelPatternsSourcesSubscriptionOptions, true).subscribe(
(subscription) => {
this.labelPatternsSourcesSubscription = subscription;
}
);
}
private labelPatternsParamsDataUpdated(data: DatasourceData[]) {
this.labelPatternsSourcesData = data;
for (let i = 0; i < this.subscription.data.length; i++) {
const series = this.subscription.data[i] as TbFlotSeries;
this.substituteLabelPatterns(series, i);
}
this.updateData();
this.ctx.detectChanges();
}
private substituteLabelPatterns(series: TbFlotSeries, seriesIndex: number) {
let seriesLabelPatternsSourcesData = this.labelPatternsSourcesData.filter((item) => {
return item.datasource.entityId === series.datasource.entityId;
});
let label = createLabelFromDatasource(series.datasource, series.dataKey.pattern);
for (let i = 0; i < seriesLabelPatternsSourcesData.length; i++) {
let keyData = seriesLabelPatternsSourcesData[i];
if (keyData && keyData.data && keyData.data[0]) {
let attrValue = keyData.data[0][1];
let attrName = keyData.dataKey.name;
if (isDefined(attrValue) && (attrValue !== null)) {
label = insertVariable(label, attrName, attrValue);
}
}
}
if (isDefined(this.subscription.legendData)) {
let targetLegendKeyIndex = this.subscription.legendData.keys.findIndex((key) => {
return key.dataIndex === seriesIndex;
});
if (targetLegendKeyIndex !== -1) {
this.subscription.legendData.keys[targetLegendKeyIndex].dataKey.label = label;
}
}
series.dataKey.label = label;
}
private seriesInfoDiv(label: string, color: string, value: any,
units: string, trackDecimals: number, active: boolean,
percent: number, valueFormatFunction: TooltipValueFormatFunction): JQuery<HTMLElement> {
const divElement = $('<div></div>');
divElement.css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
});
const lineSpan = $('<span></span>');
lineSpan.css({
backgroundColor: color,
width: '20px',
height: '3px',
display: 'inline-block',
verticalAlign: 'middle',
marginRight: '5px'
});
divElement.append(lineSpan);
const labelSpan = $(`<span>${label}:</span>`);
labelSpan.css({
marginRight: '10px'
});
if (active) {
labelSpan.css({
color: '#FFF',
fontWeight: '700'
});
}
divElement.append(labelSpan);
let valueContent: string;
if (valueFormatFunction) {
valueContent = valueFormatFunction(value);
} else {
valueContent = this.ctx.utils.formatValue(value, trackDecimals, units);
}
if (isNumber(percent)) {
valueContent += ' (' + Math.round(percent) + ' %)';
}
const valueSpan = $(`<span>${valueContent}</span>`);
valueSpan.css({
marginLeft: 'auto',
fontWeight: '700'
});
if (active) {
valueSpan.css({
color: '#FFF'
});
}
divElement.append(valueSpan);
return divElement;
}
private seriesInfoDivFromInfo(seriesHoverInfo: TbFlotSeriesHoverInfo, seriesIndex: number): string {
const units = seriesHoverInfo.units && seriesHoverInfo.units.length ? seriesHoverInfo.units : this.trackUnits;
const decimals = isDefinedAndNotNull(seriesHoverInfo.decimals) ? seriesHoverInfo.decimals : this.trackDecimals;
const divElement = this.seriesInfoDiv(seriesHoverInfo.label, seriesHoverInfo.color,
seriesHoverInfo.value, units, decimals, seriesHoverInfo.index === seriesIndex, null, seriesHoverInfo.tooltipValueFormatFunction);
return divElement.prop('outerHTML');
}
private createTooltipElement(): JQuery<any> {
const tooltip = $('<div id="flot-series-tooltip" class="flot-mouse-value"></div>');
tooltip.css({
fontSize: '12px',
fontFamily: 'Roboto',
fontWeight: '300',
lineHeight: '18px',
opacity: '1',
backgroundColor: 'rgba(0,0,0,0.7)',
color: '#D9DADB',
position: 'absolute',
display: 'none',
zIndex: '1100',
padding: '4px 10px',
borderRadius: '4px'
}).appendTo('body');
return tooltip;
}
private formatPieTooltip(item: TbFlotPlotItem): string {
const units = item.series.dataKey.units && item.series.dataKey.units.length ? item.series.dataKey.units : this.trackUnits;
const decimals = isDefinedAndNotNull(item.series.dataKey.decimals) ? item.series.dataKey.decimals : this.trackDecimals;
const divElement = this.seriesInfoDiv(item.series.dataKey.label, item.series.dataKey.color,
item.datapoint[1][0][1], units, decimals, true, item.series.percent, item.series.dataKey.tooltipValueFormatFunction);
return divElement.prop('outerHTML');
}
private formatChartTooltip(hoverInfo: TbFlotHoverInfo[], seriesIndex: number): string {
let content = '';
if (this.tooltipIndividual) {
let seriesHoverArray: TbFlotSeriesHoverInfo[];
if (hoverInfo[1] && hoverInfo[1].seriesHover.length) {
seriesHoverArray = hoverInfo[0].seriesHover.concat(hoverInfo[1].seriesHover);
} else {
seriesHoverArray = hoverInfo[0].seriesHover;
}
const found = seriesHoverArray.filter((seriesHover) => {
return seriesHover.index === seriesIndex;
});
if (found && found.length) {
const timestamp = parseInt(found[0].time, 10);
const date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
const dateDiv = $('<div>' + date + '</div>');
dateDiv.css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '4px',
fontWeight: '700'
});
content += dateDiv.prop('outerHTML');
content += this.seriesInfoDivFromInfo(found[0], seriesIndex);
}
} else {
let maxRows: number;
if (hoverInfo[1] && hoverInfo[1].seriesHover.length) {
maxRows = 5;
} else {
maxRows = 15;
}
let columns = 0;
if (hoverInfo[1] && (hoverInfo[1].seriesHover.length > hoverInfo[0].seriesHover.length)) {
columns = Math.ceil(hoverInfo[1].seriesHover.length / maxRows);
} else {
columns = Math.ceil(hoverInfo[0].seriesHover.length / maxRows);
}
hoverInfo.forEach((hoverData) => {
if (isNumber(hoverData.time)) {
let columnsContent = '';
const timestamp = parseInt(hoverData.time, 10);
const date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
const dateDiv = $('<div>' + date + '</div>');
dateDiv.css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '4px',
fontWeight: '700'
});
content += dateDiv.prop('outerHTML');
const seriesDiv = $('<div></div>');
seriesDiv.css({
display: 'flex',
flexDirection: 'row'
});
for (let c = 0; c < columns; c++) {
const columnDiv = $('<div></div>');
columnDiv.css({
display: 'flex',
flexDirection: 'column',
flex: '1'
});
let columnContent = '';
for (let i = c*maxRows; i < (c+1)*maxRows; i++) {
if (i >= hoverData.seriesHover.length) {
break;
}
const seriesHoverInfo = hoverData.seriesHover[i];
columnContent += this.seriesInfoDivFromInfo(seriesHoverInfo, seriesIndex);
}
columnDiv.html(columnContent);
if (columnContent) {
if (c > 0) {
columnsContent += '<span style="min-width: 20px;"></span>';
}
columnsContent += columnDiv.prop('outerHTML');
}
}
seriesDiv.html(columnsContent);
content += seriesDiv.prop('outerHTML');
}
});
}
return content;
}
private formatYAxisTicks(value: number, axis?: TbFlotPlotAxis): string {
if (this.settings.yaxis && this.settings.yaxis.showLabels === false) {
return '';
}
if (axis.options.ticksFormatterFunction) {
return axis.options.ticksFormatterFunction(value);
}
const factor = axis.options.tickDecimals ? Math.pow(10, axis.options.tickDecimals) : 1;
let formatted = '' + Math.round(value * factor) / factor;
if (isDefined(axis.options.tickDecimals) && axis.options.tickDecimals !== null) {
const decimal = formatted.indexOf('.');
const precision = decimal === -1 ? 0 : formatted.length - decimal - 1;
if (precision < axis.options.tickDecimals) {
formatted = (precision ? formatted : formatted + '.') + ('' + factor).substr(1, axis.options.tickDecimals - precision);
}
}
if (axis.options.tickUnits) {
formatted += ' ' + axis.options.tickUnits;
}
return formatted;
}
private enableMouseEvents() {
this.$element.css('pointer-events', '');
this.$element.addClass('mouse-events');
this.options.selection = { mode : 'x' };
this.$element.bind('plothover', this.flotHoverHandler);
this.$element.bind('plotselected', this.flotSelectHandler);
this.$element.bind('dblclick', this.dblclickHandler);
this.$element.bind('mousedown', this.mousedownHandler);
this.$element.bind('mouseup', this.mouseupHandler);
this.$element.bind('mouseleave', this.mouseleaveHandler);
this.$element.bind('plotclick', this.flotClickHandler);
}
private disableMouseEvents() {
this.$element.css('pointer-events', 'none');
this.$element.removeClass('mouse-events');
this.options.selection = { mode : null };
this.$element.unbind('plothover', this.flotHoverHandler);
this.$element.unbind('plotselected', this.flotSelectHandler);
this.$element.unbind('dblclick', this.dblclickHandler);
this.$element.unbind('mousedown', this.mousedownHandler);
this.$element.unbind('mouseup', this.mouseupHandler);
this.$element.unbind('mouseleave', this.mouseleaveHandler);
this.$element.unbind('plotclick', this.flotClickHandler);
}
private onFlotHover(e: any, pos: JQueryPlotPoint, item: TbFlotPlotItem) {
if (!this.plot) {
return;
}
if (!this.tooltipIndividual || item) {
const multipleModeTooltip = !this.tooltipIndividual;
if (multipleModeTooltip) {
this.plot.unhighlight();
}
const pageX = pos.pageX;
const pageY = pos.pageY;
let tooltipHtml;
let hoverInfo: TbFlotHoverInfo[];
if (this.chartType === 'pie') {
tooltipHtml = this.formatPieTooltip(item);
} else {
hoverInfo = this.getHoverInfo(this.plot.getData(), pos);
if (isNumber(hoverInfo[0].time) || (hoverInfo[1] && isNumber(hoverInfo[1].time))) {
hoverInfo[0].seriesHover.sort((a, b) => {
return b.value - a.value;
});
if (hoverInfo[1] && hoverInfo[1].seriesHover.length) {
hoverInfo[1].seriesHover.sort((a, b) => {
return b.value - a.value;
});
}
tooltipHtml = this.formatChartTooltip(hoverInfo, item ? item.seriesIndex : -1);
}
}
if (tooltipHtml) {
this.tooltip.html(tooltipHtml)
.css({top: 0, left: 0})
.fadeIn(200);
const windowWidth = $( window ).width();
const windowHeight = $( window ).height();
const tooltipWidth = this.tooltip.width();
const tooltipHeight = this.tooltip.height();
let left = pageX + 5;
let top = pageY + 5;
if (windowWidth - pageX < tooltipWidth + 50) {
left = pageX - tooltipWidth - 10;
}
if (windowHeight - pageY < tooltipHeight + 20) {
top = pageY - tooltipHeight - 10;
}
this.tooltip.css({
top,
left
});
if (multipleModeTooltip) {
hoverInfo.forEach((hoverData) => {
hoverData.seriesHover.forEach((seriesHoverInfo) => {
this.plot.highlight(seriesHoverInfo.index, seriesHoverInfo.hoverIndex);
});
});
}
}
} else {
this.tooltip.stop(true);
this.tooltip.hide();
this.plot.unhighlight();
}
}
private onFlotSelect(e: any, ranges: JQueryPlotSelectionRanges) {
if (!this.plot) {
return;
}
this.plot.clearSelection();
this.subscription.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to);
}
private onFlotDblClick() {
this.subscription.onResetTimewindow();
}
private onFlotMouseDown() {
this.isMouseInteraction = true;
}
private onFlotMouseUp() {
this.isMouseInteraction = false;
}
private onFlotMouseLeave() {
if (!this.tooltip) {
return;
}
this.tooltip.stop(true);
this.tooltip.hide();
if (this.plot) {
this.plot.unhighlight();
}
this.isMouseInteraction = false;
}
private onFlotClick(e: any, pos: JQueryPlotPoint, item: TbFlotPlotItem) {
if (!this.plot) {
return;
}
this.onPieSliceClick(e, item);
}
private getHoverInfo(seriesList: TbFlotPlotDataSeries[], pos: JQueryPlotPoint): TbFlotHoverInfo[] {
let i: number;
let series: TbFlotPlotDataSeries;
let hoverIndex: number;
let hoverDistance: number;
let minDistance: number;
let pointTime: any;
let minTime: any;
let minTimeHistorical: any;
let hoverData: TbFlotSeriesHoverInfo;
let value: any;
let lastValue: any;
let minDistanceHistorical: number;
const results: TbFlotHoverInfo[] = [{
seriesHover: []
}];
if (this.settings.comparisonEnabled) {
results.push({
seriesHover: []
});
}
for (i = 0; i < seriesList.length; i++) {
series = seriesList[i];
let posx: number;
if (series.datasource.isAdditional) {
posx = pos.x2;
} else {
posx = pos.x;
}
hoverIndex = this.findHoverIndexFromData(posx, series);
if (series.data[hoverIndex] && series.data[hoverIndex][0]) {
hoverDistance = posx - series.data[hoverIndex][0];
pointTime = series.data[hoverIndex][0];
if (series.datasource.isAdditional) {
if (!minDistanceHistorical
|| (hoverDistance >= 0 && (hoverDistance < minDistanceHistorical || minDistanceHistorical < 0))
|| (hoverDistance < 0 && hoverDistance > minDistanceHistorical)) {
minDistanceHistorical = hoverDistance;
minTimeHistorical = pointTime;
}
} else if (!minDistance
|| (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0))
|| (hoverDistance < 0 && hoverDistance > minDistance)) {
minDistance = hoverDistance;
minTime = pointTime;
}
if (series.stack) {
if (this.tooltipIndividual || !this.tooltipCumulative) {
value = series.data[hoverIndex][1];
} else {
lastValue += series.data[hoverIndex][1];
value = lastValue;
}
} else {
value = series.data[hoverIndex][1];
}
if (series.stack || (series.curvedLines && series.curvedLines.apply)) {
hoverIndex = this.findHoverIndexFromDataPoints(posx, series, hoverIndex);
}
if (!this.hideZeros || value) {
hoverData = {
value,
hoverIndex,
color: series.dataKey.color,
label: series.dataKey.label,
units: series.dataKey.units,
decimals: series.dataKey.decimals,
tooltipValueFormatFunction: series.dataKey.tooltipValueFormatFunction,
time: pointTime,
distance: hoverDistance,
index: i
};
if (series.datasource.isAdditional) {
results[1].seriesHover.push(hoverData);
} else {
results[0].seriesHover.push(hoverData);
}
}
}
}
if (results[1] && results[1].seriesHover.length) {
results[1].time = minTimeHistorical;
}
results[0].time = minTime;
return results;
}
private findHoverIndexFromData(posX: number, series: TbFlotPlotDataSeries): number {
let lower = 0;
let upper = series.data.length - 1;
let middle: number;
const index: number = null;
while (index === null) {
if (lower > upper) {
return Math.max(upper, 0);
}
middle = Math.floor((lower + upper) / 2);
if (series.data[middle][0] === posX) {
return middle;
} else if (series.data[middle][0] < posX) {
lower = middle + 1;
} else {
upper = middle - 1;
}
}
}
private findHoverIndexFromDataPoints(posX: number, series: TbFlotPlotDataSeries, last: number): number {
const ps = series.datapoints.pointsize;
const initial = last * ps;
const len = series.datapoints.points.length;
let j: number;
for (j = initial; j < len; j += ps) {
if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null)
|| series.datapoints.points[j] > posX) {
return Math.max(j - ps, 0) / ps;
}
}
return j / ps - 1;
}
pieDataRendered() {
for (let i = 0; i < this.pieTargetData.length; i++) {
const value = this.pieTargetData[i] ? this.pieTargetData[i] : 0;
this.pieRenderedData[i] = value;
if (!this.pieData[i].data[0]) {
this.pieData[i].data[0] = [0, 0];
}
this.pieData[i].data[0][1] = value;
}
}
nextPieDataAnimation(start) {
if (start) {
this.finishPieDataAnimation();
this.pieAnimationStartTime = this.pieAnimationLastTime = Date.now();
for (let i = 0; i < this.subscription.data.length; i++) {
this.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0])
? this.subscription.data[i].data[0][1] : 0;
}
}
if (this.pieAnimationCaf) {
this.pieAnimationCaf();
this.pieAnimationCaf = null;
}
this.pieAnimationCaf = this.ctx.$scope.raf.raf(this.onPieDataAnimation.bind(this));
}
onPieDataAnimation() {
const time = Date.now();
const elapsed = time - this.pieAnimationLastTime; // this.pieAnimationStartTime;
const progress = (time - this.pieAnimationStartTime) / this.pieDataAnimationDuration;
if (progress >= 1) {
this.finishPieDataAnimation();
} else {
if (elapsed >= 40) {
for (let i = 0; i < this.pieTargetData.length; i++) {
const prevValue = this.pieRenderedData[i];
const targetValue = this.pieTargetData[i];
const value = prevValue + (targetValue - prevValue) * progress;
if (!this.pieData[i].data[0]) {
this.pieData[i].data[0] = [0, 0];
}
this.pieData[i].data[0][1] = value;
}
this.plot.setData(this.pieData);
this.plot.draw();
this.pieAnimationLastTime = time;
}
this.nextPieDataAnimation(false);
}
}
private finishPieDataAnimation() {
this.pieDataRendered();
this.plot.setData(this.pieData);
this.plot.draw();
}
private onPieSliceClick($event: any, item: TbFlotPlotItem) {
const descriptors = this.ctx.actionsApi.getActionDescriptors('sliceClick');
if ($event && descriptors.length) {
$event.stopPropagation();
const entityInfo = this.ctx.actionsApi.getActiveEntityInfo();
const entityId = entityInfo ? entityInfo.entityId : null;
const entityName = entityInfo ? entityInfo.entityName : null;
const entityLabel = entityInfo ? entityInfo.entityLabel : null;
this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, item, entityLabel);
}
}
}