Merge pull request #10535 from thingsboard/feature/state-chart
State chart widget
This commit is contained in:
commit
18178b5243
@ -12,10 +12,11 @@
|
||||
"line_chart",
|
||||
"bar_chart",
|
||||
"point_chart",
|
||||
"state_chart",
|
||||
"bar_chart_with_labels",
|
||||
"range_chart",
|
||||
"charts.basic_timeseries",
|
||||
"charts.state_chart",
|
||||
"range_chart",
|
||||
"bar_chart_with_labels",
|
||||
"charts.timeseries_bars_flot",
|
||||
"cards.aggregated_value_card",
|
||||
"charts.bars",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,25 @@
|
||||
{
|
||||
"fqn": "charts.state_chart",
|
||||
"name": "State Chart",
|
||||
"deprecated": true,
|
||||
"image": "tb-image:c3RhdGVfY2hhcnRfc3lzdGVtX3dpZGdldF9pbWFnZS5wbmc=:IlN0YXRlIENoYXJ0IiBzeXN0ZW0gd2lkZ2V0IGltYWdl;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAB9VBMVEUAAAAhlvMilvMymeE+nNVDoetInslNq/VZqOdpuPd3d3d5p5Z5suB6enp8fHyBgYGDg4ODqoqEhISIiIiKioqKuN2MjIyNjY2Ojo6QkJCRkZGSkpKUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKCgr3KhoaGioqKisXSjo6OjvsmkpKSlpaWlwdempqanp6eoqKiosGOo1vqpqampsGOqqqqqsWOrq6usrKysw9Wurq6wsLCysrK0tLS1tbW1wbK2tra3t7e4wau5ubm6urq7u7u8vLy9vb2+vr6/xbTAwMDAxbjCwsLC3ejDw8PExMTFxcXGxsbHx8fIv3jIyMjJycnKysrK5vzMzc7Nzc3Ozs7Pz8/QuDnRzcHS0tLT09PT39HU1NTU6/3VzLLV1dXV3sjW1tbX19fYuTHY2NjZ2dna0Ira2trb29vc3Nzex4De3t7f39/guyvhvCfhvCvh4eHh6Nbi4uLjvCTj4+PkyXbk5OTlvCTm5ubm693o6Ojpxl7q6urr6+vswTTs7Ozt7e3uvhju7u7vvhjv7+/wvxjw8PDx8fHyvxX0yTv09PT19fX29vb39/f4wyL4+Pj5+fn6+vr75J37+/v8whP8/Pz9/f39/v/+/v7/wQf/xRb/3HT/5JH/9tz/++/////APs7XAAAAAWJLR0Smt7AblQAAA1RJREFUeNrt3dlT01AUBvAEd8UNl2oLrdrFolZArUul1hWlVhQXFAUF1xaxIqi4FUQUrDsUClZi4nb+Th96S9NQkjDOOKZ+3wuZw8md++M25OXMlKOccJQnTUYocdRqdw8UAqTfOjG4lvr7uowOqbtAtG6o5OiGFoNDapuJHO8tFK4xOKS9msTVgoUiaQjPclnSl01snZ/y4ly2yBNZbRarbc+3yvdpt/hLawOPJp9ur7O0mRiEmzFkY1M6N/4I8qJpulzR2sBx1sgRjQuyA2pk+STdylw2VqR/FPFfWdcCvjhduiM7kVF2db1Nuspu7JGes+I76SRba7f0Wfnn/6F6It+mfo7m8TvYvh7KTiT3PQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggPwXkFa7e7AQIP3WiUFzdl7LuBDFvJZxIYp5LeNCFPNaBn7Yc+e1KlheSYcrFCniL7LZqPl8cbp0TjavNcqu9rZJB9gdPdIJVnwjbWG1ndI95UzWM9V5rS9Ti3P4VWy1+5jXAgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAMK+FeS3Ma2FeC/NamNfCCxEQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBA/hZkTKSkwSGn7VUJOmOLV241NiRSRZ2uYcvt+Io+Y0N83USm+MqGWEnU2JCqXqI1ZE+QhYiIy36tXR5INpOQbB5kftcmK85mtdey2iJekeWqX6/3kc+TLCQTrq6eEuZCgKTsbnNXBsIZOERJMedNQnry73XpW8UAKVhIyBPScVfQE9RqEaP7iMT6g+pdw7vKbxKl3IJqV8K7OUqXPHvG9UMGykk2lz1d4g5yvNXo6QqZiA41aHT5OwQTUWBJSrWrvlMwj1jFU2f1Q8K1dCysCUmVhdenNLvMRKZt3hGNriEnxfxlGqt9aPYRkb9bP6Q1SMFrmltMukKuMT2QpckWjc/WhP2lYB/TgjzdHyCKVM/gGXnsp+qY5hbba+hIVA/ETL0+9SepsoNiTs/igGpXZIhKqVv9QVJARIfXIWqfiM1nS+qBnHdae1V7Qss8ngEijRO5a6sMCAtdqv+HfgPwpNPbU6ipOwAAAABJRU5ErkJggg==",
|
||||
"description": "Displays changes to the state of the entity over time. For example, online and offline.",
|
||||
"descriptor": {
|
||||
"type": "timeseries",
|
||||
"sizeX": 8,
|
||||
"sizeY": 5,
|
||||
"resources": [],
|
||||
"templateHtml": "<tb-flot-widget \n [ctx]=\"ctx\" chartType=\"state\">\n</tb-flot-widget>",
|
||||
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
|
||||
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n\n",
|
||||
"settingsSchema": "{}",
|
||||
"dataKeySettingsSchema": "{}",
|
||||
"settingsDirective": "tb-flot-line-widget-settings",
|
||||
"dataKeySettingsDirective": "tb-flot-line-key-settings",
|
||||
"latestDataKeySettingsDirective": "tb-flot-latest-key-settings",
|
||||
"hasBasicMode": true,
|
||||
"basicModeDirective": "tb-flot-basic-config",
|
||||
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}"
|
||||
},
|
||||
"tags": null
|
||||
}
|
||||
@ -378,6 +378,7 @@ export class DataAggregator {
|
||||
private updateStateBounds(keyData: DataSet, lastPrevKvPair: DataEntry) {
|
||||
if (lastPrevKvPair) {
|
||||
lastPrevKvPair[0] = this.startTs;
|
||||
lastPrevKvPair[2] = [this.startTs, this.startTs];
|
||||
}
|
||||
let firstKvPair: DataEntry;
|
||||
if (!keyData.length) {
|
||||
@ -398,6 +399,7 @@ export class DataAggregator {
|
||||
if (lastKvPair[0] < this.endTs) {
|
||||
lastKvPair = deepClone(lastKvPair);
|
||||
lastKvPair[0] = this.endTs;
|
||||
lastKvPair[2] = [this.endTs, this.endTs];
|
||||
keyData.push(lastKvPair);
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ import {
|
||||
import { TimeService } from '../services/time.service';
|
||||
import { DeviceService } from '../http/device.service';
|
||||
import { UtilsService } from '@core/services/utils.service';
|
||||
import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
|
||||
import { 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';
|
||||
@ -303,6 +303,7 @@ export interface IWidgetSubscription {
|
||||
hiddenData?: Array<{data: DataSet}>;
|
||||
timeWindowConfig?: Timewindow;
|
||||
timeWindow?: WidgetTimewindow;
|
||||
subscriptionTimewindow: SubscriptionTimewindow;
|
||||
onTimewindowChangeFunction?: (timewindow: Timewindow) => Timewindow;
|
||||
widgetTimewindowChanged$: Observable<WidgetTimewindow>;
|
||||
comparisonEnabled?: boolean;
|
||||
|
||||
@ -41,6 +41,10 @@
|
||||
[entityAliasId]="datasource?.entityAliasId"
|
||||
formControlName="series">
|
||||
</tb-data-keys-panel>
|
||||
<tb-time-series-chart-states-panel
|
||||
*ngIf="chartType === TimeSeriesChartType.state"
|
||||
formControlName="states">
|
||||
</tb-time-series-chart-states-panel>
|
||||
<tb-time-series-chart-y-axes-panel
|
||||
formControlName="yAxes"
|
||||
(axisRemoved)="yAxisRemoved($event)">
|
||||
|
||||
@ -46,8 +46,9 @@ import {
|
||||
} from '@home/components/widget/lib/chart/time-series-chart-widget.models';
|
||||
import { EChartsTooltipTrigger } from '@home/components/widget/lib/chart/echarts-widget.models';
|
||||
import {
|
||||
TimeSeriesChartKeySettings, TimeSeriesChartThreshold,
|
||||
TimeSeriesChartType, TimeSeriesChartYAxes,
|
||||
TimeSeriesChartKeySettings,
|
||||
TimeSeriesChartType,
|
||||
TimeSeriesChartYAxes,
|
||||
TimeSeriesChartYAxisId
|
||||
} from '@home/components/widget/lib/chart/time-series-chart.models';
|
||||
|
||||
@ -178,6 +179,9 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
|
||||
|
||||
actions: [configData.config.actions || {}, []]
|
||||
});
|
||||
if (this.chartType === TimeSeriesChartType.state) {
|
||||
this.timeSeriesChartWidgetConfigForm.addControl('states', this.fb.control(settings.states, []));
|
||||
}
|
||||
}
|
||||
|
||||
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
|
||||
@ -233,6 +237,10 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
|
||||
this.widgetConfig.config.settings.padding = config.padding;
|
||||
|
||||
this.widgetConfig.config.actions = config.actions;
|
||||
|
||||
if (this.chartType === TimeSeriesChartType.state) {
|
||||
this.widgetConfig.config.settings.states = config.states;
|
||||
}
|
||||
return this.widgetConfig;
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
import * as echarts from 'echarts/core';
|
||||
import AxisModel from 'echarts/types/src/coord/cartesian/AxisModel';
|
||||
import { estimateLabelUnionRect } from 'echarts/lib/coord/axisHelper';
|
||||
import { formatValue, isDefinedAndNotNull, isNumber } from '@core/utils';
|
||||
import { isDefinedAndNotNull, isFunction, isNumber } from '@core/utils';
|
||||
import {
|
||||
DataZoomComponent,
|
||||
DataZoomComponentOption,
|
||||
@ -42,7 +42,7 @@ import {
|
||||
} from 'echarts/charts';
|
||||
import { LabelLayout } from 'echarts/features';
|
||||
import { CanvasRenderer, SVGRenderer } from 'echarts/renderers';
|
||||
import { DataEntry, DataKey, DataSet } from '@shared/models/widget.models';
|
||||
import { DataEntry, DataKey, DataSet, Datasource, FormattedData } from '@shared/models/widget.models';
|
||||
import {
|
||||
calculateAggIntervalWithWidgetTimeWindow,
|
||||
Interval,
|
||||
@ -56,6 +56,7 @@ import GlobalModel from 'echarts/types/src/model/Global';
|
||||
import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D';
|
||||
import SeriesModel from 'echarts/types/src/model/Series';
|
||||
import { MarkLine2DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel';
|
||||
import { TimeAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes';
|
||||
|
||||
class EChartsModule {
|
||||
private initialized = false;
|
||||
@ -101,14 +102,19 @@ export type EChartsDataItem = [number, any, number, number];
|
||||
|
||||
export type NamedDataSet = {name: string; value: EChartsDataItem}[];
|
||||
|
||||
export type EChartsTooltipValueFormatFunction = (value: any, latestData: FormattedData, units?: string, decimals?: number) => string;
|
||||
|
||||
export type EChartsSeriesItem = {
|
||||
id: string;
|
||||
datasource: Datasource;
|
||||
dataKey: DataKey;
|
||||
data: NamedDataSet;
|
||||
dataSet?: DataSet;
|
||||
enabled: boolean;
|
||||
units?: string;
|
||||
decimals?: number;
|
||||
latestData?: FormattedData;
|
||||
tooltipValueFormatFunction?: EChartsTooltipValueFormatFunction;
|
||||
};
|
||||
|
||||
export enum EChartsShape {
|
||||
@ -270,7 +276,7 @@ const measureSymbolOffset = (symbol: string, symbolSize: any): number => {
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const measureThresholdOffset = (chart: ECharts, axisId: string, thresholdId: string, value: any): [number, number] => {
|
||||
const offset: [number, number] = [0,0];
|
||||
@ -395,7 +401,7 @@ export const getFocusedSeriesIndex = (chart: ECharts): number => {
|
||||
return -1;
|
||||
};
|
||||
|
||||
export const toNamedData = (data: DataSet): NamedDataSet => {
|
||||
export const toNamedData = (data: DataSet, valueConverter?: (value: any) => any): NamedDataSet => {
|
||||
if (!data?.length) {
|
||||
return [];
|
||||
} else {
|
||||
@ -403,14 +409,43 @@ export const toNamedData = (data: DataSet): NamedDataSet => {
|
||||
const ts = isDefinedAndNotNull(d[2]) ? d[2][0] : d[0];
|
||||
return {
|
||||
name: ts + '',
|
||||
value: toEChartsDataItem(d)
|
||||
value: toEChartsDataItem(d, valueConverter)
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toEChartsDataItem = (entry: DataEntry): EChartsDataItem => {
|
||||
const item: EChartsDataItem = [entry[0], entry[1], entry[0], entry[0]];
|
||||
const minDataTs = (dataSet: NamedDataSet): number => dataSet.length ? dataSet.map(data =>
|
||||
Number(data.name)).reduce((a, b) => Math.min(a, b)) : undefined;
|
||||
|
||||
const maxDataTs = (dataSet: NamedDataSet): number => dataSet.length ? dataSet.map(data =>
|
||||
Number(data.name)).reduce((a, b) => Math.max(a, b)) : undefined;
|
||||
|
||||
export const adjustTimeAxisExtentToData = (timeAxisOption: TimeAxisBaseOption,
|
||||
dataItems: EChartsSeriesItem[],
|
||||
defaultMin: number,
|
||||
defaultMax: number): void => {
|
||||
let min: number;
|
||||
let max: number;
|
||||
for (const item of dataItems) {
|
||||
if (item.enabled) {
|
||||
const minTs = minDataTs(item.data);
|
||||
if (typeof minTs !== 'undefined') {
|
||||
min = (typeof min !== 'undefined') ? Math.min(min, minTs) : minTs;
|
||||
}
|
||||
const maxTs = maxDataTs(item.data);
|
||||
if (typeof maxTs !== 'undefined') {
|
||||
max = (typeof max !== 'undefined') ? Math.max(max, maxTs) : maxTs;
|
||||
}
|
||||
}
|
||||
}
|
||||
timeAxisOption.min = (typeof min !== 'undefined') ? min : defaultMin;
|
||||
timeAxisOption.max = (typeof max !== 'undefined') ? max : defaultMax;
|
||||
};
|
||||
|
||||
const toEChartsDataItem = (entry: DataEntry, valueConverter?: (value: any) => any): EChartsDataItem => {
|
||||
const value = valueConverter ? valueConverter(entry[1]) : entry[1];
|
||||
const item: EChartsDataItem = [entry[0], value, entry[0], entry[0]];
|
||||
if (isDefinedAndNotNull(entry[2])) {
|
||||
item[2] = entry[2][0];
|
||||
item[3] = entry[2][1];
|
||||
@ -436,6 +471,7 @@ export interface EChartsTooltipWidgetSettings {
|
||||
tooltipShowFocusedSeries?: boolean;
|
||||
tooltipValueFont: Font;
|
||||
tooltipValueColor: string;
|
||||
tooltipValueFormatter?: string | EChartsTooltipValueFormatFunction;
|
||||
tooltipShowDate: boolean;
|
||||
tooltipDateInterval?: boolean;
|
||||
tooltipDateFormat: DateFormatSettings;
|
||||
@ -445,12 +481,25 @@ export interface EChartsTooltipWidgetSettings {
|
||||
tooltipBackgroundBlur: number;
|
||||
}
|
||||
|
||||
export const createTooltipValueFormatFunction =
|
||||
(tooltipValueFormatter: string | EChartsTooltipValueFormatFunction): EChartsTooltipValueFormatFunction => {
|
||||
let tooltipValueFormatFunction: EChartsTooltipValueFormatFunction;
|
||||
if (isFunction(tooltipValueFormatter)) {
|
||||
tooltipValueFormatFunction = tooltipValueFormatter as EChartsTooltipValueFormatFunction;
|
||||
} else if (typeof tooltipValueFormatter === 'string' && tooltipValueFormatter.length) {
|
||||
try {
|
||||
tooltipValueFormatFunction =
|
||||
new Function('value', 'latestData', tooltipValueFormatter) as EChartsTooltipValueFormatFunction;
|
||||
} catch (e) {}
|
||||
}
|
||||
return tooltipValueFormatFunction;
|
||||
};
|
||||
|
||||
export const echartsTooltipFormatter = (renderer: Renderer2,
|
||||
tooltipDateFormat: DateFormatProcessor,
|
||||
settings: EChartsTooltipWidgetSettings,
|
||||
params: CallbackDataParams[] | CallbackDataParams,
|
||||
decimals: number,
|
||||
units: string,
|
||||
valueFormatFunction: EChartsTooltipValueFormatFunction,
|
||||
focusedSeriesIndex: number,
|
||||
series?: EChartsSeriesItem[],
|
||||
interval?: Interval): null | HTMLElement => {
|
||||
@ -499,10 +548,12 @@ export const echartsTooltipFormatter = (renderer: Renderer2,
|
||||
seriesParams = params;
|
||||
}
|
||||
if (seriesParams) {
|
||||
renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units, series));
|
||||
renderer.appendChild(tooltipElement,
|
||||
constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, valueFormatFunction, series));
|
||||
} else if (Array.isArray(params)) {
|
||||
for (seriesParams of params) {
|
||||
renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units, series));
|
||||
renderer.appendChild(tooltipElement,
|
||||
constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, valueFormatFunction, series));
|
||||
}
|
||||
}
|
||||
return tooltipElement;
|
||||
@ -511,8 +562,7 @@ export const echartsTooltipFormatter = (renderer: Renderer2,
|
||||
const constructEchartsTooltipSeriesElement = (renderer: Renderer2,
|
||||
settings: EChartsTooltipWidgetSettings,
|
||||
seriesParams: CallbackDataParams,
|
||||
decimals: number,
|
||||
units: string,
|
||||
valueFormatFunction: EChartsTooltipValueFormatFunction,
|
||||
series?: EChartsSeriesItem[]): HTMLElement => {
|
||||
const labelValueElement: HTMLElement = renderer.createElement('div');
|
||||
renderer.setStyle(labelValueElement, 'display', 'flex');
|
||||
@ -542,16 +592,25 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2,
|
||||
renderer.setStyle(labelTextElement, 'color', 'rgba(0, 0, 0, 0.76)');
|
||||
renderer.appendChild(labelElement, labelTextElement);
|
||||
const valueElement: HTMLElement = renderer.createElement('div');
|
||||
let formatDecimals = decimals;
|
||||
let formatUnits = units;
|
||||
let formatFunction = valueFormatFunction;
|
||||
let latestData: FormattedData;
|
||||
let units = '';
|
||||
let decimals = 0;
|
||||
if (series) {
|
||||
const item = series.find(s => s.id === seriesParams.seriesId);
|
||||
if (item) {
|
||||
formatDecimals = item.decimals;
|
||||
formatUnits = item.units;
|
||||
if (item.tooltipValueFormatFunction) {
|
||||
formatFunction = item.tooltipValueFormatFunction;
|
||||
}
|
||||
latestData = item.latestData;
|
||||
units = item.units;
|
||||
decimals = item.decimals;
|
||||
}
|
||||
}
|
||||
const value = formatValue(seriesParams.value[1], formatDecimals, formatUnits, false);
|
||||
if (!latestData) {
|
||||
latestData = {} as FormattedData;
|
||||
}
|
||||
const value = formatFunction(seriesParams.value[1], latestData, units, decimals);
|
||||
renderer.appendChild(valueElement, renderer.createText(value));
|
||||
renderer.setStyle(valueElement, 'flex', '1');
|
||||
renderer.setStyle(valueElement, 'text-align', 'end');
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
///
|
||||
/// Copyright © 2016-2024 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 {
|
||||
TimeSeriesChartStateSettings,
|
||||
TimeSeriesChartStateSourceType,
|
||||
TimeSeriesChartTicksFormatter,
|
||||
TimeSeriesChartTicksGenerator
|
||||
} from '@home/components/widget/lib/chart/time-series-chart.models';
|
||||
import { UtilsService } from '@core/services/utils.service';
|
||||
import { EChartsTooltipValueFormatFunction } from '@home/components/widget/lib/chart/echarts-widget.models';
|
||||
import { FormattedData } from '@shared/models/widget.models';
|
||||
import { formatValue, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils';
|
||||
import { LabelFormatterCallback } from 'echarts';
|
||||
|
||||
export class TimeSeriesChartStateValueConverter {
|
||||
|
||||
private readonly constantsMap = new Map<any, number>();
|
||||
private readonly rangeStates: TimeSeriesChartStateSettings[] = [];
|
||||
private readonly ticks: {value: number}[] = [];
|
||||
private readonly labelsMap = new Map<number, string>();
|
||||
|
||||
public readonly ticksGenerator: TimeSeriesChartTicksGenerator;
|
||||
public readonly ticksFormatter: TimeSeriesChartTicksFormatter;
|
||||
public readonly tooltipFormatter: EChartsTooltipValueFormatFunction;
|
||||
public readonly labelFormatter: LabelFormatterCallback;
|
||||
public readonly valueConverter: (value: any) => any;
|
||||
|
||||
constructor(utils: UtilsService,
|
||||
states: TimeSeriesChartStateSettings[]) {
|
||||
const ticks: number[] = [];
|
||||
for (const state of states) {
|
||||
if (state.sourceType === TimeSeriesChartStateSourceType.constant) {
|
||||
this.constantsMap.set(state.sourceValue, state.value);
|
||||
} else {
|
||||
this.rangeStates.push(state);
|
||||
}
|
||||
if (!ticks.includes(state.value)) {
|
||||
ticks.push(state.value);
|
||||
const label = utils.customTranslation(state.label, state.label);
|
||||
this.labelsMap.set(state.value, label);
|
||||
}
|
||||
}
|
||||
this.ticks = ticks.map(val => ({value: val}));
|
||||
this.ticksGenerator = () => this.ticks;
|
||||
this.ticksFormatter = (value: any) => {
|
||||
const result = this.labelsMap.get(value);
|
||||
return result || '';
|
||||
};
|
||||
this.tooltipFormatter = (value: any, latestData: FormattedData, units?: string, decimals?: number) => {
|
||||
const result = this.labelsMap.get(value);
|
||||
if (typeof result === 'string') {
|
||||
return result;
|
||||
} else {
|
||||
return formatValue(value, decimals, units, false);
|
||||
}
|
||||
};
|
||||
this.labelFormatter = (params) => {
|
||||
const value = params.value[1];
|
||||
const result = this.labelsMap.get(value);
|
||||
if (typeof result === 'string') {
|
||||
return `{value|${result}}`;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
this.valueConverter = (value: any) => {
|
||||
let key = value;
|
||||
if (key === 'true') {
|
||||
key = true;
|
||||
} else if (key === 'false') {
|
||||
key = false;
|
||||
}
|
||||
const result = this.constantsMap.get(key);
|
||||
if (typeof result === 'number') {
|
||||
return result;
|
||||
} else if (this.rangeStates.length && isDefinedAndNotNull(value) && isNumeric(value)) {
|
||||
for (const state of this.rangeStates) {
|
||||
const num = Number(value);
|
||||
if (TimeSeriesChartStateValueConverter.constantRange(state) && state.sourceRangeFrom === num) {
|
||||
return state.value;
|
||||
} else if ((!isNumber(state.sourceRangeFrom) || num >= state.sourceRangeFrom) &&
|
||||
(!isNumber(state.sourceRangeTo) || num < state.sourceRangeTo)) {
|
||||
return state.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
static constantRange(state: TimeSeriesChartStateSettings): boolean {
|
||||
return isNumber(state.sourceRangeFrom) && isNumber(state.sourceRangeTo) && state.sourceRangeFrom === state.sourceRangeTo;
|
||||
}
|
||||
|
||||
}
|
||||
@ -17,8 +17,10 @@
|
||||
import {
|
||||
ECharts,
|
||||
EChartsOption,
|
||||
EChartsSeriesItem, EChartsShape,
|
||||
EChartsSeriesItem,
|
||||
EChartsShape,
|
||||
EChartsTooltipTrigger,
|
||||
EChartsTooltipValueFormatFunction,
|
||||
EChartsTooltipWidgetSettings,
|
||||
measureThresholdOffset,
|
||||
timeAxisBandWidthCalculator
|
||||
@ -41,7 +43,8 @@ import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts';
|
||||
import {
|
||||
formatValue,
|
||||
isDefinedAndNotNull,
|
||||
isFunction, isNumber,
|
||||
isFunction,
|
||||
isNumber,
|
||||
isNumeric,
|
||||
isUndefined,
|
||||
isUndefinedOrNull,
|
||||
@ -51,7 +54,8 @@ import {
|
||||
import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { ValueAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes';
|
||||
import { LabelFormatterCallback, LabelLayoutOption, SeriesLabelOption } from 'echarts/types/src/util/types';
|
||||
import { LabelLayoutOption, SeriesLabelOption } from 'echarts/types/src/util/types';
|
||||
import { LabelFormatterCallback } from 'echarts';
|
||||
import {
|
||||
BarRenderContext,
|
||||
BarRenderSharedContext,
|
||||
@ -70,7 +74,8 @@ export enum TimeSeriesChartType {
|
||||
default = 'default',
|
||||
line = 'line',
|
||||
bar = 'bar',
|
||||
point = 'point'
|
||||
point = 'point',
|
||||
state = 'state'
|
||||
}
|
||||
|
||||
export const timeSeriesChartTypeTranslations = new Map<TimeSeriesChartType, string>(
|
||||
@ -200,6 +205,21 @@ export const timeSeriesThresholdTypeTranslations = new Map<TimeSeriesChartThresh
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
export enum TimeSeriesChartStateSourceType {
|
||||
constant = 'constant',
|
||||
range = 'range'
|
||||
}
|
||||
|
||||
export const timeSeriesStateSourceTypes = Object.keys(TimeSeriesChartStateSourceType) as TimeSeriesChartStateSourceType[];
|
||||
|
||||
export const timeSeriesStateSourceTypeTranslations = new Map<TimeSeriesChartStateSourceType, string>(
|
||||
[
|
||||
[TimeSeriesChartStateSourceType.constant, 'widgets.time-series-chart.state.type-constant'],
|
||||
[TimeSeriesChartStateSourceType.range, 'widgets.time-series-chart.state.type-range']
|
||||
]
|
||||
);
|
||||
|
||||
export enum SeriesFillType {
|
||||
none = 'none',
|
||||
opacity = 'opacity',
|
||||
@ -639,6 +659,39 @@ export interface TimeSeriesChartVisualMapSettings {
|
||||
pieces: TimeSeriesChartVisualMapPiece[];
|
||||
}
|
||||
|
||||
export interface TimeSeriesChartStateSettings {
|
||||
label: string;
|
||||
value: number;
|
||||
sourceType: TimeSeriesChartStateSourceType;
|
||||
sourceValue?: any;
|
||||
sourceRangeFrom?: number;
|
||||
sourceRangeTo?: number;
|
||||
}
|
||||
|
||||
export const timeSeriesChartStateValid = (state: TimeSeriesChartStateSettings): boolean => {
|
||||
if (isUndefinedOrNull(state.value) || !state.sourceType) {
|
||||
return false;
|
||||
}
|
||||
switch (state.sourceType) {
|
||||
case TimeSeriesChartStateSourceType.constant:
|
||||
if (isUndefinedOrNull(state.sourceValue)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const timeSeriesChartStateValidator = (control: AbstractControl): ValidationErrors | null => {
|
||||
const state: TimeSeriesChartStateSettings = control.value;
|
||||
if (!timeSeriesChartStateValid(state)) {
|
||||
return {
|
||||
state: true
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings {
|
||||
thresholds: TimeSeriesChartThreshold[];
|
||||
darkMode: boolean;
|
||||
@ -650,6 +703,7 @@ export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings {
|
||||
barWidthSettings: TimeSeriesChartBarWidthSettings;
|
||||
noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings;
|
||||
visualMapSettings?: TimeSeriesChartVisualMapSettings;
|
||||
states?: TimeSeriesChartStateSettings[];
|
||||
}
|
||||
|
||||
export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = {
|
||||
@ -751,6 +805,7 @@ export interface TimeSeriesChartKeySettings {
|
||||
type: TimeSeriesChartSeriesType;
|
||||
lineSettings: LineSeriesSettings;
|
||||
barSettings: BarSeriesSettings;
|
||||
tooltipValueFormatter?: string | EChartsTooltipValueFormatFunction;
|
||||
}
|
||||
|
||||
export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = {
|
||||
@ -1359,28 +1414,28 @@ const createSeriesLabelOption = (item: TimeSeriesChartDataItem, show: boolean,
|
||||
if (show) {
|
||||
labelStyle = createChartTextStyle(labelFont, labelColor, darkMode, 'series.label', labelColorFill);
|
||||
}
|
||||
let formatter: LabelFormatterCallback;
|
||||
if (isFunction(labelFormatter)) {
|
||||
formatter = labelFormatter as LabelFormatterCallback;
|
||||
} else if (labelFormatter?.length) {
|
||||
const formatFunction = parseFunction(labelFormatter, ['value']);
|
||||
formatter = (params): string => {
|
||||
let result: string;
|
||||
try {
|
||||
result = formatFunction(params.value[1]);
|
||||
} catch (_e) {
|
||||
}
|
||||
if (isUndefined(result)) {
|
||||
result = formatValue(params.value[1], item.decimals, item.units, false);
|
||||
}
|
||||
return `{value|${result}}`;
|
||||
};
|
||||
} else {
|
||||
formatter = (params): string => {
|
||||
const value = formatValue(params.value[1], item.decimals, item.units, false);
|
||||
return `{value|${value}}`;
|
||||
};
|
||||
let formatFunction: (...args: any[]) => any;
|
||||
if (typeof labelFormatter === 'string' && labelFormatter.length) {
|
||||
formatFunction = parseFunction(labelFormatter, ['value']);
|
||||
}
|
||||
const formatter: LabelFormatterCallback = (params): string => {
|
||||
let result: string;
|
||||
if (typeof labelFormatter === 'string') {
|
||||
if (formatFunction) {
|
||||
try {
|
||||
result = formatFunction(params.value[1]);
|
||||
} catch (_e) {
|
||||
}
|
||||
}
|
||||
} else if (isFunction(labelFormatter)) {
|
||||
result = labelFormatter(params);
|
||||
}
|
||||
if (isUndefined(result)) {
|
||||
result = formatValue(params.value[1], item.decimals, item.units, false);
|
||||
result = `{value|${result}}`;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const labelOption: SeriesLabelOption = {
|
||||
show,
|
||||
position,
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
createTimeSeriesYAxis,
|
||||
defaultTimeSeriesChartYAxisSettings,
|
||||
generateChartData,
|
||||
LineSeriesStepType,
|
||||
parseThresholdData,
|
||||
SeriesLabelPosition,
|
||||
TimeSeriesChartDataItem,
|
||||
@ -44,13 +45,17 @@ import {
|
||||
} from '@home/components/widget/lib/chart/time-series-chart.models';
|
||||
import { ResizeObserver } from '@juggle/resize-observer';
|
||||
import {
|
||||
adjustTimeAxisExtentToData,
|
||||
calculateXAxisHeight,
|
||||
calculateYAxisWidth,
|
||||
createTooltipValueFormatFunction,
|
||||
ECharts,
|
||||
echartsModule,
|
||||
EChartsOption, EChartsShape,
|
||||
EChartsOption,
|
||||
EChartsShape,
|
||||
echartsTooltipFormatter,
|
||||
EChartsTooltipTrigger,
|
||||
EChartsTooltipValueFormatFunction,
|
||||
getAxisExtent,
|
||||
getFocusedSeriesIndex,
|
||||
measureXAxisNameHeight,
|
||||
@ -58,12 +63,11 @@ import {
|
||||
toNamedData
|
||||
} from '@home/components/widget/lib/chart/echarts-widget.models';
|
||||
import { DateFormatProcessor } from '@shared/models/widget-settings.models';
|
||||
import { isDefinedAndNotNull, isEqual, mergeDeep } from '@core/utils';
|
||||
import { DataKey, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models';
|
||||
import { formattedDataFormDatasourceData, formatValue, isDefinedAndNotNull, isEqual, mergeDeep } from '@core/utils';
|
||||
import { DataKey, Datasource, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { CallbackDataParams, PiecewiseVisualMapOption } from 'echarts/types/dist/shared';
|
||||
import { Renderer2 } from '@angular/core';
|
||||
import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { AggregationType } from '@shared/models/time/time.models';
|
||||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
|
||||
@ -71,6 +75,7 @@ import { WidgetSubscriptionOptions } from '@core/api/widget-api.models';
|
||||
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
|
||||
import { DeepPartial } from '@shared/models/common';
|
||||
import { BarRenderSharedContext } from '@home/components/widget/lib/chart/time-series-chart-bar.models';
|
||||
import { TimeSeriesChartStateValueConverter } from '@home/components/widget/lib/chart/time-series-chart-state.models';
|
||||
|
||||
export class TbTimeSeriesChart {
|
||||
|
||||
@ -89,6 +94,13 @@ export class TbTimeSeriesChart {
|
||||
settings.lineSettings.showPoints = true;
|
||||
settings.lineSettings.pointShape = EChartsShape.circle;
|
||||
settings.lineSettings.pointSize = 8;
|
||||
} else if (type === TimeSeriesChartType.state) {
|
||||
settings.type = TimeSeriesChartSeriesType.line;
|
||||
settings.lineSettings.showLine = true;
|
||||
settings.lineSettings.step = true;
|
||||
settings.lineSettings.stepType = LineSeriesStepType.end;
|
||||
settings.lineSettings.pointShape = EChartsShape.circle;
|
||||
settings.lineSettings.pointSize = 12;
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
@ -97,7 +109,11 @@ export class TbTimeSeriesChart {
|
||||
}
|
||||
|
||||
private get noAggregation(): boolean {
|
||||
return this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE;
|
||||
return this.ctx.defaultSubscription.subscriptionTimewindow?.aggregation?.type === AggregationType.NONE;
|
||||
}
|
||||
|
||||
private get stateData(): boolean {
|
||||
return this.ctx.defaultSubscription.subscriptionTimewindow?.aggregation?.stateData === true;
|
||||
}
|
||||
|
||||
private readonly shapeResize$: ResizeObserver;
|
||||
@ -115,6 +131,8 @@ export class TbTimeSeriesChart {
|
||||
private timeSeriesChartOptions: EChartsOption;
|
||||
|
||||
private readonly tooltipDateFormat: DateFormatProcessor;
|
||||
private readonly tooltipValueFormatFunction: EChartsTooltipValueFormatFunction;
|
||||
private readonly stateValueConverter: TimeSeriesChartStateValueConverter;
|
||||
|
||||
private yMinSubject = new BehaviorSubject(-1);
|
||||
private yMaxSubject = new BehaviorSubject(1);
|
||||
@ -131,6 +149,8 @@ export class TbTimeSeriesChart {
|
||||
|
||||
private barRenderSharedContext: BarRenderSharedContext;
|
||||
|
||||
private latestData: FormattedData[] = [];
|
||||
|
||||
yMin$ = this.yMinSubject.asObservable();
|
||||
yMax$ = this.yMaxSubject.asObservable();
|
||||
|
||||
@ -143,6 +163,10 @@ export class TbTimeSeriesChart {
|
||||
this.settings = mergeDeep({} as TimeSeriesChartSettings,
|
||||
timeSeriesChartDefaultSettings,
|
||||
this.inputSettings as TimeSeriesChartSettings);
|
||||
if (this.settings.states && this.settings.states.length) {
|
||||
this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.dashboard.utils, this.settings.states);
|
||||
this.tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter;
|
||||
}
|
||||
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');
|
||||
@ -150,8 +174,17 @@ export class TbTimeSeriesChart {
|
||||
this.setupData();
|
||||
this.setupThresholds();
|
||||
this.setupVisualMap();
|
||||
if (this.settings.showTooltip && this.settings.tooltipShowDate) {
|
||||
this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat);
|
||||
if (this.settings.showTooltip) {
|
||||
if (this.settings.tooltipShowDate) {
|
||||
this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat);
|
||||
}
|
||||
if (!this.tooltipValueFormatFunction) {
|
||||
this.tooltipValueFormatFunction =
|
||||
createTooltipValueFormatFunction(this.settings.tooltipValueFormatter);
|
||||
if (!this.tooltipValueFormatFunction) {
|
||||
this.tooltipValueFormatFunction = (value, latestData, units, decimals) => formatValue(value, decimals, units, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.onResize();
|
||||
if (this.autoResize) {
|
||||
@ -178,7 +211,7 @@ export class TbTimeSeriesChart {
|
||||
const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === item.dataKey) : null;
|
||||
if (!isEqual(item.dataSet, datasourceData?.data)) {
|
||||
item.dataSet = datasourceData?.data;
|
||||
item.data = datasourceData?.data ? toNamedData(datasourceData.data) : [];
|
||||
item.data = datasourceData?.data ? toNamedData(datasourceData.data, this.stateValueConverter?.valueConverter) : [];
|
||||
}
|
||||
}
|
||||
this.onResize();
|
||||
@ -205,6 +238,14 @@ export class TbTimeSeriesChart {
|
||||
public latestUpdated() {
|
||||
let update = false;
|
||||
if (this.ctx.latestData) {
|
||||
this.latestData = formattedDataFormDatasourceData(this.ctx.latestData);
|
||||
for (const item of this.dataItems) {
|
||||
let latestData = this.latestData.find(data => data.$datasource === item.datasource);
|
||||
if (!latestData) {
|
||||
latestData = {} as FormattedData;
|
||||
}
|
||||
item.latestData = latestData;
|
||||
}
|
||||
for (const item of this.thresholdItems) {
|
||||
if (item.settings.type === TimeSeriesChartThresholdType.latestKey && item.latestDataKey) {
|
||||
const data = this.ctx.latestData.find(d => d.dataKey === item.latestDataKey);
|
||||
@ -253,7 +294,7 @@ export class TbTimeSeriesChart {
|
||||
seriesId: dataItem.id
|
||||
});
|
||||
}
|
||||
this.timeSeriesChartOptions.series = this.updateSeries();
|
||||
this.updateSeries();
|
||||
const mergeList = ['series'];
|
||||
if (this.updateYAxisScale(this.yAxisList)) {
|
||||
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
|
||||
@ -338,9 +379,12 @@ export class TbTimeSeriesChart {
|
||||
.includes(keySettings.barSettings.labelPosition as SeriesLabelPosition))) {
|
||||
this.topPointLabels = true;
|
||||
}
|
||||
if (this.stateValueConverter && keySettings.type === TimeSeriesChartSeriesType.line) {
|
||||
keySettings.lineSettings.pointLabelFormatter = this.stateValueConverter.labelFormatter;
|
||||
}
|
||||
dataKey.settings = keySettings;
|
||||
const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === dataKey) : null;
|
||||
const namedData = datasourceData?.data ? toNamedData(datasourceData.data) : [];
|
||||
const namedData = datasourceData?.data ? toNamedData(datasourceData.data, this.stateValueConverter?.valueConverter) : [];
|
||||
const units = dataKey.units && dataKey.units.length ? dataKey.units : this.ctx.units;
|
||||
const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals :
|
||||
(isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2);
|
||||
@ -354,9 +398,11 @@ export class TbTimeSeriesChart {
|
||||
decimals,
|
||||
yAxisId,
|
||||
yAxisIndex: this.getYAxisIndex(yAxisId),
|
||||
datasource,
|
||||
dataKey,
|
||||
data: namedData,
|
||||
enabled: !keySettings.dataHiddenByDefault
|
||||
enabled: !keySettings.dataHiddenByDefault,
|
||||
tooltipValueFormatFunction: createTooltipValueFormatFunction(keySettings.tooltipValueFormatter)
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -451,6 +497,10 @@ export class TbTimeSeriesChart {
|
||||
const units = axisSettings.units && axisSettings.units.length ? axisSettings.units : this.ctx.units;
|
||||
const decimals = isDefinedAndNotNull(axisSettings.decimals) ? axisSettings.decimals :
|
||||
(isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2);
|
||||
if (this.stateValueConverter) {
|
||||
axisSettings.ticksGenerator = this.stateValueConverter.ticksGenerator;
|
||||
axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter;
|
||||
}
|
||||
const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.darkMode);
|
||||
this.yAxisList.push(yAxis);
|
||||
}
|
||||
@ -528,7 +578,7 @@ export class TbTimeSeriesChart {
|
||||
},
|
||||
formatter: (params: CallbackDataParams[]) =>
|
||||
this.settings.showTooltip ? echartsTooltipFormatter(this.renderer, this.tooltipDateFormat,
|
||||
this.settings, params, 0, '',
|
||||
this.settings, params, this.tooltipValueFormatFunction,
|
||||
this.settings.tooltipShowFocusedSeries ? getFocusedSeriesIndex(this.timeSeriesChart) : -1,
|
||||
this.dataItems, this.noAggregation ? null : this.ctx.timeWindow.interval) : undefined,
|
||||
padding: [8, 12],
|
||||
@ -551,13 +601,15 @@ export class TbTimeSeriesChart {
|
||||
{
|
||||
type: 'inside',
|
||||
disabled: !this.settings.dataZoom,
|
||||
realtime: true
|
||||
realtime: true,
|
||||
filterMode: this.stateData ? 'none' : 'filter'
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
show: this.settings.dataZoom,
|
||||
showDetail: false,
|
||||
realtime: true,
|
||||
filterMode: this.stateData ? 'none' : 'filter',
|
||||
bottom: 10
|
||||
}
|
||||
],
|
||||
@ -577,7 +629,7 @@ export class TbTimeSeriesChart {
|
||||
|
||||
this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow;
|
||||
|
||||
this.timeSeriesChartOptions.series = this.updateSeries();
|
||||
this.updateSeries();
|
||||
if (this.updateYAxisScale(this.yAxisList)) {
|
||||
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
|
||||
}
|
||||
@ -593,7 +645,7 @@ export class TbTimeSeriesChart {
|
||||
}
|
||||
|
||||
private updateSeriesData(updateScale = false): void {
|
||||
this.timeSeriesChartOptions.series = this.updateSeries();
|
||||
this.updateSeries();
|
||||
if (updateScale && this.updateYAxisScale(this.yAxisList)) {
|
||||
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
|
||||
}
|
||||
@ -601,11 +653,16 @@ export class TbTimeSeriesChart {
|
||||
this.updateAxes();
|
||||
}
|
||||
|
||||
private updateSeries(): Array<LineSeriesOption | CustomSeriesOption> {
|
||||
return generateChartData(this.dataItems, this.thresholdItems,
|
||||
private updateSeries(): void {
|
||||
this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems,
|
||||
this.settings.stack,
|
||||
this.noAggregation,
|
||||
this.barRenderSharedContext, this.darkMode);
|
||||
if (this.stateData) {
|
||||
adjustTimeAxisExtentToData(this.timeSeriesChartOptions.xAxis[0], this.dataItems,
|
||||
this.ctx.defaultSubscription.timeWindow.minTime,
|
||||
this.ctx.defaultSubscription.timeWindow.maxTime);
|
||||
}
|
||||
}
|
||||
|
||||
private updateAxes(lazy = true) {
|
||||
|
||||
@ -56,6 +56,16 @@
|
||||
formControlName="barSettings">
|
||||
</tb-time-series-chart-bar-settings>
|
||||
</div>
|
||||
<div class="tb-form-panel">
|
||||
<div class="tb-form-panel-title" translate>widgets.chart.tooltip-settings</div>
|
||||
<tb-js-func
|
||||
formControlName="tooltipValueFormatter"
|
||||
[globalVariables]="functionScopeVariables"
|
||||
[functionArgs]="['value', 'latestData']"
|
||||
functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}"
|
||||
helpId="widget/lib/flot/tooltip_value_format_fn">
|
||||
</tb-js-func>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #chartTypeTitle>
|
||||
<div class="tb-form-panel-title">{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}</div>
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
} from '@home/components/widget/lib/chart/time-series-chart.models';
|
||||
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
|
||||
import { TimeSeriesChartWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-widget.models';
|
||||
import { WidgetService } from '@core/http/widget.service';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-time-series-chart-key-settings',
|
||||
@ -53,7 +54,10 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
|
||||
|
||||
yAxisIds: TimeSeriesChartYAxisId[];
|
||||
|
||||
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private widgetService: WidgetService,
|
||||
private fb: UntypedFormBuilder) {
|
||||
super(store);
|
||||
}
|
||||
@ -88,7 +92,8 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
|
||||
dataHiddenByDefault: [seriesSettings.dataHiddenByDefault, []],
|
||||
type: [seriesSettings.type, []],
|
||||
lineSettings: [seriesSettings.lineSettings, []],
|
||||
barSettings: [seriesSettings.barSettings, []]
|
||||
barSettings: [seriesSettings.barSettings, []],
|
||||
tooltipValueFormatter: [seriesSettings.tooltipValueFormatter, []],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
<ng-container [formGroup]="lineSettingsFormGroup">
|
||||
<div *ngIf="chartType !== TimeSeriesChartType.point" class="tb-form-panel stroked">
|
||||
<div class="tb-form-panel-title" translate>widgets.time-series-chart.series.line.line</div>
|
||||
<div class="tb-form-row">
|
||||
<div *ngIf="chartType !== TimeSeriesChartType.state" class="tb-form-row">
|
||||
<mat-slide-toggle class="mat-slide" formControlName="showLine">
|
||||
{{ 'widgets.time-series-chart.series.line.show-line' | translate }}
|
||||
</mat-slide-toggle>
|
||||
@ -35,7 +35,7 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="tb-form-row">
|
||||
<div *ngIf="chartType !== TimeSeriesChartType.state" class="tb-form-row">
|
||||
<mat-slide-toggle class="mat-slide" formControlName="smooth">
|
||||
{{ 'widgets.time-series-chart.series.line.smooth-line' | translate }}
|
||||
</mat-slide-toggle>
|
||||
|
||||
@ -147,12 +147,22 @@ export class TimeSeriesChartLineSettingsComponent implements OnInit, ControlValu
|
||||
}
|
||||
|
||||
private updateValidators() {
|
||||
const state = this.chartType === TimeSeriesChartType.state;
|
||||
const showLine: boolean = this.lineSettingsFormGroup.get('showLine').value;
|
||||
const step: boolean = this.lineSettingsFormGroup.get('step').value;
|
||||
const showPointLabel: boolean = this.lineSettingsFormGroup.get('showPointLabel').value;
|
||||
const enablePointLabelBackground: boolean = this.lineSettingsFormGroup.get('enablePointLabelBackground').value;
|
||||
if (state) {
|
||||
this.lineSettingsFormGroup.get('showLine').disable({emitEvent: false});
|
||||
} else {
|
||||
this.lineSettingsFormGroup.get('showLine').enable({emitEvent: false});
|
||||
}
|
||||
if (showLine) {
|
||||
this.lineSettingsFormGroup.get('step').enable({emitEvent: false});
|
||||
if (state) {
|
||||
this.lineSettingsFormGroup.get('step').disable({emitEvent: false});
|
||||
} else {
|
||||
this.lineSettingsFormGroup.get('step').enable({emitEvent: false});
|
||||
}
|
||||
if (step) {
|
||||
this.lineSettingsFormGroup.get('stepType').enable({emitEvent: false});
|
||||
this.lineSettingsFormGroup.get('smooth').disable({emitEvent: false});
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
|
||||
-->
|
||||
<ng-container [formGroup]="timeSeriesChartWidgetSettingsForm">
|
||||
<tb-time-series-chart-states-panel
|
||||
*ngIf="chartType === TimeSeriesChartType.state"
|
||||
formControlName="states">
|
||||
</tb-time-series-chart-states-panel>
|
||||
<tb-time-series-chart-y-axes-panel
|
||||
formControlName="yAxes"
|
||||
(axisRemoved)="yAxisRemoved($event)"
|
||||
@ -118,6 +122,13 @@
|
||||
</tb-color-input>
|
||||
</div>
|
||||
</div>
|
||||
<tb-js-func
|
||||
formControlName="tooltipValueFormatter"
|
||||
[globalVariables]="functionScopeVariables"
|
||||
[functionArgs]="['value', 'latestData']"
|
||||
functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}"
|
||||
helpId="widget/lib/flot/tooltip_value_format_fn">
|
||||
</tb-js-func>
|
||||
<div class="tb-form-row column-xs">
|
||||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="tooltipShowDate">
|
||||
{{ 'tooltip.date' | translate }}
|
||||
|
||||
@ -39,6 +39,7 @@ import {
|
||||
TimeSeriesChartYAxisId
|
||||
} from '@home/components/widget/lib/chart/time-series-chart.models';
|
||||
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
|
||||
import { WidgetService } from '@core/http/widget.service';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-time-series-chart-widget-settings',
|
||||
@ -77,8 +78,11 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
|
||||
|
||||
chartType: TimeSeriesChartType = TimeSeriesChartType.default;
|
||||
|
||||
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private $injector: Injector,
|
||||
private widgetService: WidgetService,
|
||||
private fb: UntypedFormBuilder) {
|
||||
super(store);
|
||||
}
|
||||
@ -129,6 +133,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
|
||||
tooltipTrigger: [settings.tooltipTrigger, []],
|
||||
tooltipValueFont: [settings.tooltipValueFont, []],
|
||||
tooltipValueColor: [settings.tooltipValueColor, []],
|
||||
tooltipValueFormatter: [settings.tooltipValueFormatter, []],
|
||||
tooltipShowDate: [settings.tooltipShowDate, []],
|
||||
tooltipDateFormat: [settings.tooltipDateFormat, []],
|
||||
tooltipDateFont: [settings.tooltipDateFont, []],
|
||||
@ -143,6 +148,9 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
|
||||
background: [settings.background, []],
|
||||
padding: [settings.padding, []]
|
||||
});
|
||||
if (this.chartType === TimeSeriesChartType.state) {
|
||||
this.timeSeriesChartWidgetSettingsForm.addControl('states', this.fb.control(settings.states, []));
|
||||
}
|
||||
}
|
||||
|
||||
protected validatorTriggers(): string[] {
|
||||
@ -168,6 +176,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipTrigger').enable();
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFont').enable();
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueColor').enable();
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFormatter').enable();
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').enable({emitEvent: false});
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundColor').enable();
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundBlur').enable();
|
||||
@ -185,6 +194,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
|
||||
} else {
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFont').disable();
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueColor').disable();
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFormatter').disable();
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').disable({emitEvent: false});
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFormat').disable();
|
||||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFont').disable();
|
||||
|
||||
@ -68,6 +68,15 @@
|
||||
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="axisType === 'yAxis' && advanced" class="tb-form-row">
|
||||
<tb-js-func fxFlex
|
||||
formControlName="ticksGenerator"
|
||||
[globalVariables]="functionScopeVariables"
|
||||
[functionArgs]="['extent']"
|
||||
functionTitle="{{ 'widgets.time-series-chart.axis.ticks-generator-function' | translate }}"
|
||||
helpId="widget/lib/chart/ticks_generator_fn">
|
||||
</tb-js-func>
|
||||
</div>
|
||||
<div class="tb-form-panel no-padding stroked">
|
||||
<div class="tb-form-row no-border space-between column-xs">
|
||||
<mat-slide-toggle class="mat-slide" formControlName="showTickLabels">
|
||||
|
||||
@ -85,7 +85,7 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
|
||||
public axisSettingsFormGroup: UntypedFormGroup;
|
||||
|
||||
constructor(private fb: UntypedFormBuilder,
|
||||
private widgetService: WidgetService,) {
|
||||
private widgetService: WidgetService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -113,6 +113,7 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
|
||||
this.axisSettingsFormGroup.addControl('units', this.fb.control(null, []));
|
||||
this.axisSettingsFormGroup.addControl('decimals', this.fb.control(null, [Validators.min(0)]));
|
||||
this.axisSettingsFormGroup.addControl('ticksFormatter', this.fb.control(null, []));
|
||||
this.axisSettingsFormGroup.addControl('ticksGenerator', this.fb.control(null, []));
|
||||
this.axisSettingsFormGroup.addControl('interval', this.fb.control(null, [Validators.min(0)]));
|
||||
this.axisSettingsFormGroup.addControl('splitNumber', this.fb.control(null, [Validators.min(1)]));
|
||||
this.axisSettingsFormGroup.addControl('min', this.fb.control(null, []));
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
<!--
|
||||
|
||||
Copyright © 2016-2024 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.
|
||||
|
||||
-->
|
||||
<div [formGroup]="stateFormGroup" class="tb-form-table-row tb-time-series-state-row">
|
||||
<mat-form-field appearance="outline" class="tb-inline-field tb-state-label-field" subscriptSizing="dynamic">
|
||||
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="tb-inline-field number tb-state-value-field" subscriptSizing="dynamic">
|
||||
<input matInput formControlName="value" type="number" placeholder="{{ 'widget-config.set' | translate }}">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="tb-inline-field tb-state-source-field" appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-select formControlName="sourceType">
|
||||
<mat-option *ngFor="let type of timeSeriesStateSourceTypes" [value]="type">
|
||||
{{ timeSeriesStateSourceTypeTranslations.get(type) | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div class="tb-state-source-value-field">
|
||||
<tb-value-input
|
||||
*ngIf="stateFormGroup.get('sourceType').value === TimeSeriesChartStateSourceType.constant"
|
||||
formControlName="sourceValue"
|
||||
fxFlex
|
||||
[layout]="{
|
||||
layout: 'column',
|
||||
breakpoints: {'gt-sm': 'row'}
|
||||
}">
|
||||
</tb-value-input>
|
||||
<mat-form-field
|
||||
*ngIf="stateFormGroup.get('sourceType').value === TimeSeriesChartStateSourceType.range"
|
||||
appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">
|
||||
<input matInput formControlName="sourceRangeFrom" type="number" placeholder="{{ 'widgets.time-series-chart.state.from' | translate }}">
|
||||
</mat-form-field>
|
||||
<mat-form-field
|
||||
*ngIf="stateFormGroup.get('sourceType').value === TimeSeriesChartStateSourceType.range"
|
||||
appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">
|
||||
<input matInput formControlName="sourceRangeTo" type="number" placeholder="{{ 'widgets.time-series-chart.state.to' | translate }}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="tb-form-table-row-cell-buttons">
|
||||
<button type="button"
|
||||
mat-icon-button
|
||||
(click)="stateRemoved.emit()"
|
||||
matTooltip="{{ 'widgets.time-series-chart.state.remove-state' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright © 2016-2024 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 '../../../../../../../../../scss/constants';
|
||||
|
||||
.tb-form-table-row.tb-time-series-state-row {
|
||||
@media #{$mat-lt-md} {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.tb-state-label-field {
|
||||
flex: 1;
|
||||
min-width: 70px;
|
||||
}
|
||||
.tb-state-value-field {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
}
|
||||
.tb-state-source-field {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
}
|
||||
.tb-state-source-value-field {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
@media #{$mat-gt-sm} {
|
||||
min-width: 358px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.tb-inline-field {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
///
|
||||
/// Copyright © 2016-2024 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 {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
NG_VALUE_ACCESSOR,
|
||||
UntypedFormBuilder,
|
||||
UntypedFormGroup,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
TimeSeriesChartStateSettings,
|
||||
TimeSeriesChartStateSourceType,
|
||||
timeSeriesStateSourceTypes,
|
||||
timeSeriesStateSourceTypeTranslations
|
||||
} from '@home/components/widget/lib/chart/time-series-chart.models';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-time-series-chart-state-row',
|
||||
templateUrl: './time-series-chart-state-row.component.html',
|
||||
styleUrls: ['./time-series-chart-state-row.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => TimeSeriesChartStateRowComponent),
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class TimeSeriesChartStateRowComponent implements ControlValueAccessor, OnInit {
|
||||
|
||||
TimeSeriesChartStateSourceType = TimeSeriesChartStateSourceType;
|
||||
|
||||
timeSeriesStateSourceTypes = timeSeriesStateSourceTypes;
|
||||
|
||||
timeSeriesStateSourceTypeTranslations = timeSeriesStateSourceTypeTranslations;
|
||||
|
||||
@Input()
|
||||
disabled: boolean;
|
||||
|
||||
@Output()
|
||||
stateRemoved = new EventEmitter();
|
||||
|
||||
stateFormGroup: UntypedFormGroup;
|
||||
|
||||
modelValue: TimeSeriesChartStateSettings;
|
||||
|
||||
private propagateChange = (_val: any) => {};
|
||||
|
||||
constructor(private fb: UntypedFormBuilder,
|
||||
private cd: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.stateFormGroup = this.fb.group({
|
||||
label: [null, []],
|
||||
value: [null, [Validators.required]],
|
||||
sourceType: [null, [Validators.required]],
|
||||
sourceValue: [null, [Validators.required]],
|
||||
sourceRangeFrom: [null, []],
|
||||
sourceRangeTo: [null, []]
|
||||
});
|
||||
this.stateFormGroup.valueChanges.subscribe(
|
||||
() => this.updateModel()
|
||||
);
|
||||
this.stateFormGroup.get('sourceType').valueChanges.subscribe(() => {
|
||||
this.updateValidators();
|
||||
});
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.propagateChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
if (isDisabled) {
|
||||
this.stateFormGroup.disable({emitEvent: false});
|
||||
} else {
|
||||
this.stateFormGroup.enable({emitEvent: false});
|
||||
this.updateValidators();
|
||||
}
|
||||
}
|
||||
|
||||
writeValue(value: TimeSeriesChartStateSettings): void {
|
||||
this.modelValue = value;
|
||||
this.stateFormGroup.patchValue(
|
||||
value, {emitEvent: false}
|
||||
);
|
||||
this.updateValidators();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
private updateValidators() {
|
||||
const sourceType: TimeSeriesChartStateSourceType = this.stateFormGroup.get('sourceType').value;
|
||||
if (sourceType === TimeSeriesChartStateSourceType.constant) {
|
||||
this.stateFormGroup.get('sourceValue').enable({emitEvent: false});
|
||||
this.stateFormGroup.get('sourceRangeFrom').disable({emitEvent: false});
|
||||
this.stateFormGroup.get('sourceRangeTo').disable({emitEvent: false});
|
||||
} else if (sourceType === TimeSeriesChartStateSourceType.range) {
|
||||
this.stateFormGroup.get('sourceValue').disable({emitEvent: false});
|
||||
this.stateFormGroup.get('sourceRangeFrom').enable({emitEvent: false});
|
||||
this.stateFormGroup.get('sourceRangeTo').enable({emitEvent: false});
|
||||
}
|
||||
}
|
||||
|
||||
private updateModel() {
|
||||
this.modelValue = this.stateFormGroup.value;
|
||||
this.propagateChange(this.modelValue);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
<!--
|
||||
|
||||
Copyright © 2016-2024 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.
|
||||
|
||||
-->
|
||||
<div class="tb-form-panel tb-time-series-states-panel">
|
||||
<div class="tb-form-panel-title">{{ 'widgets.time-series-chart.state.states' | translate }}</div>
|
||||
<div class="tb-form-table">
|
||||
<div class="tb-form-table-header">
|
||||
<div class="tb-form-table-header-cell tb-state-label-header" translate>widgets.time-series-chart.state.label</div>
|
||||
<div class="tb-form-table-header-cell tb-state-value-header" translate>widgets.time-series-chart.state.ticks-value</div>
|
||||
<div class="tb-form-table-header-cell tb-state-source-header" translate>widgets.time-series-chart.state.source</div>
|
||||
<div class="tb-form-table-header-cell tb-state-source-value-header" translate>widgets.time-series-chart.state.value-range</div>
|
||||
<div class="tb-form-table-header-cell tb-actions-header"></div>
|
||||
</div>
|
||||
<div *ngIf="statesFormArray().controls.length; else noStates" class="tb-form-table-body">
|
||||
<div *ngFor="let stateControl of statesFormArray().controls; trackBy: trackByState; let $index = index; let $last = last">
|
||||
<tb-time-series-chart-state-row fxFlex
|
||||
[formControl]="stateControl"
|
||||
(stateRemoved)="removeState($index)">
|
||||
</tb-time-series-chart-state-row>
|
||||
<mat-divider *ngIf="!$last"></mat-divider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" mat-stroked-button color="primary" (click)="addState()">
|
||||
{{ 'widgets.time-series-chart.state.add-state' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #noStates>
|
||||
<span fxLayoutAlign="center center"
|
||||
class="tb-prompt">{{ 'widgets.time-series-chart.state.no-states' | translate }}</span>
|
||||
</ng-template>
|
||||
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright © 2016-2024 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 '../../../../../../../../../scss/constants';
|
||||
|
||||
.tb-time-series-states-panel {
|
||||
.tb-form-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.tb-form-table-header-cell {
|
||||
&.tb-state-label-header {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
}
|
||||
&.tb-state-value-header {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
}
|
||||
&.tb-state-source-header {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
}
|
||||
&.tb-state-source-value-header {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
@media #{$mat-gt-sm} {
|
||||
min-width: 358px;
|
||||
}
|
||||
}
|
||||
&.tb-actions-header {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
}
|
||||
}
|
||||
.tb-form-table-header {
|
||||
min-width: fit-content;
|
||||
}
|
||||
.tb-form-table-body {
|
||||
min-width: fit-content;
|
||||
.mat-divider {
|
||||
margin-top: 8px;
|
||||
@media #{$mat-gt-sm} {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
///
|
||||
/// Copyright © 2016-2024 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 { Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
ControlValueAccessor,
|
||||
NG_VALIDATORS,
|
||||
NG_VALUE_ACCESSOR,
|
||||
UntypedFormArray,
|
||||
UntypedFormBuilder,
|
||||
UntypedFormControl,
|
||||
UntypedFormGroup,
|
||||
Validator
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
TimeSeriesChartStateSettings,
|
||||
TimeSeriesChartStateSourceType,
|
||||
timeSeriesChartStateValid,
|
||||
timeSeriesChartStateValidator
|
||||
} from '@home/components/widget/lib/chart/time-series-chart.models';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-time-series-chart-states-panel',
|
||||
templateUrl: './time-series-chart-states-panel.component.html',
|
||||
styleUrls: ['./time-series-chart-states-panel.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => TimeSeriesChartStatesPanelComponent),
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => TimeSeriesChartStatesPanelComponent),
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class TimeSeriesChartStatesPanelComponent implements ControlValueAccessor, OnInit, Validator {
|
||||
|
||||
@Input()
|
||||
disabled: boolean;
|
||||
|
||||
statesFormGroup: UntypedFormGroup;
|
||||
|
||||
private propagateChange = (_val: any) => {};
|
||||
|
||||
constructor(private fb: UntypedFormBuilder) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.statesFormGroup = this.fb.group({
|
||||
states: [this.fb.array([]), []]
|
||||
});
|
||||
this.statesFormGroup.valueChanges.subscribe(
|
||||
() => {
|
||||
let states: TimeSeriesChartStateSettings[] = this.statesFormGroup.get('states').value;
|
||||
if (states) {
|
||||
states = states.filter(s => timeSeriesChartStateValid(s));
|
||||
}
|
||||
this.propagateChange(states);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.propagateChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
if (isDisabled) {
|
||||
this.statesFormGroup.disable({emitEvent: false});
|
||||
} else {
|
||||
this.statesFormGroup.enable({emitEvent: false});
|
||||
}
|
||||
}
|
||||
|
||||
writeValue(value: TimeSeriesChartStateSettings[] | undefined): void {
|
||||
const states = value || [];
|
||||
this.statesFormGroup.setControl('states', this.prepareStatesFormArray(states), {emitEvent: false});
|
||||
}
|
||||
|
||||
public validate(c: UntypedFormControl) {
|
||||
const valid = this.statesFormGroup.valid;
|
||||
return valid ? null : {
|
||||
states: {
|
||||
valid: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
statesFormArray(): UntypedFormArray {
|
||||
return this.statesFormGroup.get('states') as UntypedFormArray;
|
||||
}
|
||||
|
||||
trackByState(index: number, stateControl: AbstractControl): any {
|
||||
return stateControl;
|
||||
}
|
||||
|
||||
removeState(index: number) {
|
||||
(this.statesFormGroup.get('states') as UntypedFormArray).removeAt(index);
|
||||
}
|
||||
|
||||
addState() {
|
||||
const state: TimeSeriesChartStateSettings = {
|
||||
label: '',
|
||||
value: 0,
|
||||
sourceType: TimeSeriesChartStateSourceType.constant
|
||||
};
|
||||
const statesArray = this.statesFormGroup.get('states') as UntypedFormArray;
|
||||
const stateControl = this.fb.control(state, [timeSeriesChartStateValidator]);
|
||||
statesArray.push(stateControl);
|
||||
}
|
||||
|
||||
private prepareStatesFormArray(states: TimeSeriesChartStateSettings[] | undefined): UntypedFormArray {
|
||||
const statesControls: Array<AbstractControl> = [];
|
||||
if (states) {
|
||||
states.forEach((state) => {
|
||||
statesControls.push(this.fb.control(state, [timeSeriesChartStateValidator]));
|
||||
});
|
||||
}
|
||||
return this.fb.array(statesControls);
|
||||
}
|
||||
|
||||
}
|
||||
@ -133,6 +133,12 @@ import {
|
||||
import {
|
||||
TimeSeriesChartThresholdSettingsComponent
|
||||
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component';
|
||||
import {
|
||||
TimeSeriesChartStateRowComponent
|
||||
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component';
|
||||
import {
|
||||
TimeSeriesChartStatesPanelComponent
|
||||
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -182,6 +188,8 @@ import {
|
||||
TimeSeriesChartAnimationSettingsComponent,
|
||||
TimeSeriesChartFillSettingsComponent,
|
||||
TimeSeriesChartThresholdSettingsComponent,
|
||||
TimeSeriesChartStatesPanelComponent,
|
||||
TimeSeriesChartStateRowComponent,
|
||||
DataKeyInputComponent,
|
||||
EntityAliasInputComponent
|
||||
],
|
||||
@ -237,6 +245,8 @@ import {
|
||||
TimeSeriesChartAnimationSettingsComponent,
|
||||
TimeSeriesChartFillSettingsComponent,
|
||||
TimeSeriesChartThresholdSettingsComponent,
|
||||
TimeSeriesChartStatesPanelComponent,
|
||||
TimeSeriesChartStateRowComponent,
|
||||
DataKeyInputComponent,
|
||||
EntityAliasInputComponent
|
||||
],
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
#### Ticks generator function
|
||||
|
||||
<div class="divider"></div>
|
||||
<br/>
|
||||
|
||||
*function (extent): {value: number}[]*
|
||||
|
||||
A JavaScript function used to generate Y axis ticks.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
<ul>
|
||||
<li><b>extent:</b> <code>number[]</code> - An array of two numbers holding axis min and max values <b>[axisMin, axisMax]</b>.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
**Returns:**
|
||||
|
||||
An array of tick values with the following structure:
|
||||
|
||||
```typescript
|
||||
{
|
||||
value: number
|
||||
}
|
||||
```
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
##### Examples
|
||||
|
||||
* Always display only one tick in the middle:
|
||||
|
||||
```javascript
|
||||
return extent ? [{ value: (extent[0] + extent[1]) / 2}] : [];
|
||||
{:copy-code}
|
||||
```
|
||||
|
||||
* Display only min and max ticks:
|
||||
|
||||
```javascript
|
||||
if (extent) {
|
||||
return [ {value: extent[0]}, {value: extent[1]} ];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
{:copy-code}
|
||||
```
|
||||
|
||||
* Disable ticks:
|
||||
|
||||
```javascript
|
||||
return [];
|
||||
{:copy-code}
|
||||
```
|
||||
|
||||
* Constant ticks (1,2,3):
|
||||
|
||||
```javascript
|
||||
return [ {value: 1}, {value: 2}, {value: 3} ];
|
||||
{:copy-code}
|
||||
```
|
||||
@ -6783,6 +6783,20 @@
|
||||
"label-position-inside-end-bottom": "Inside end bottom",
|
||||
"label-background": "Label background"
|
||||
},
|
||||
"state": {
|
||||
"states": "States",
|
||||
"label": "Label",
|
||||
"ticks-value": "Ticks value",
|
||||
"source": "Source",
|
||||
"value-range": "Value / Range",
|
||||
"no-states": "No states configured",
|
||||
"add-state": "Add state",
|
||||
"type-constant": "Constant",
|
||||
"type-range": "Range",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"remove-state": "Remove state"
|
||||
},
|
||||
"axis": {
|
||||
"axes": "Axes",
|
||||
"x-axis": "X axis",
|
||||
@ -6798,6 +6812,7 @@
|
||||
"position-bottom": "Bottom",
|
||||
"tick-labels": "Tick labels",
|
||||
"ticks-formatter-function": "Ticks formatter function",
|
||||
"ticks-generator-function": "Ticks generator function",
|
||||
"show-ticks": "Show ticks",
|
||||
"show-line": "Show line",
|
||||
"show-split-lines": "Show split lines",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user