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