28 changed files with 1053 additions and 82 deletions
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 |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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} |
||||
|
``` |
||||
Loading…
Reference in new issue