diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts index d95056a3a3..5712f31e52 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts @@ -15,12 +15,12 @@ /// import * as CanvasGauges from 'canvas-gauges'; -import GenericOptions = CanvasGauges.GenericOptions; -import BaseGauge = CanvasGauges.BaseGauge; import { FontStyle, FontWeight } from '@home/components/widget/lib/settings.models'; import * as tinycolor_ from 'tinycolor2'; import { ColorFormats } from 'tinycolor2'; import { isDefined, isString, isUndefined } from '@core/utils'; +import GenericOptions = CanvasGauges.GenericOptions; +import BaseGauge = CanvasGauges.BaseGauge; const tinycolor = tinycolor_; @@ -32,12 +32,12 @@ export interface DigitalGaugeColorRange { rgbString: string; } -export interface colorLevelSetting { +export interface ColorLevelSetting { value: number; color: string; } -export type levelColors = Array; +export type levelColors = Array; export interface CanvasDigitalGaugeOptions extends GenericOptions { gaugeType?: GaugeType; @@ -81,6 +81,11 @@ export interface CanvasDigitalGaugeOptions extends GenericOptions { fontValueHeight?: FontHeightInfo; fontMinMaxHeight?: FontHeightInfo; + ticksValue?: number[]; + ticks?: number[]; + colorTicks?: string; + tickWidth?: number; + showTimestamp?: boolean; } @@ -117,6 +122,10 @@ const defaultDigitalGaugeOptions: CanvasDigitalGaugeOptions = { ...GenericOption neonGlowBrightness: 0, + colorTicks: 'gray', + tickWidth: 4, + ticks: [], + isMobile: false } }; @@ -243,9 +252,14 @@ export class CanvasDigitalGauge extends BaseGauge { options.neonColorsRange = []; } for (let i = 0; i < options.levelColors.length; i++) { - let levelColor: any = options.levelColors[i]; + const levelColor: any = options.levelColors[i]; if (levelColor !== null) { - let percentage = isColorProperty ? inc * i : CanvasDigitalGauge.normalizeValue(levelColor.value, options.minValue, options.maxValue); + let percentage: number; + if(isColorProperty){ + percentage = inc * i; + } else { + percentage = CanvasDigitalGauge.normalizeValue(levelColor.value, options.minValue, options.maxValue); + } let tColor = tinycolor(isColorProperty ? levelColor : levelColor.color); options.colorsRange.push({ pct: percentage, @@ -263,6 +277,13 @@ export class CanvasDigitalGauge extends BaseGauge { } } + options.ticksValue = []; + for (const tick of options.ticks) { + if (tick !== null) { + options.ticksValue.push(CanvasDigitalGauge.normalizeValue(tick, options.minValue, options.maxValue)) + } + } + if (options.neonGlowBrightness) { options.neonColorTitle = tinycolor(options.colorTitle).brighten(options.neonGlowBrightness).toHexString(); options.neonColorLabel = tinycolor(options.colorLabel).brighten(options.neonGlowBrightness).toHexString(); @@ -274,7 +295,7 @@ export class CanvasDigitalGauge extends BaseGauge { } static normalizeValue (value: number, min: number, max: number): number { - let normalValue = (value - min) / (max - min); + const normalValue = (value - min) / (max - min); if (normalValue <= 0) { return 0; } @@ -657,7 +678,7 @@ function determineFontHeight (options: CanvasDigitalGaugeOptions, target: string fontStyle: options['font' + target + 'Style'] }; const text = $('Hg').css(fontStyle); - const block = $('
'); + const block = $('
'); const div = $('
'); div.append(text, block); @@ -884,6 +905,52 @@ function drawBarGlow(context: DigitalGaugeCanvasRenderingContext2D, startX: numb context.stroke(); } +function drawTickArc(context: DigitalGaugeCanvasRenderingContext2D, tickValues: number[], Cx: number, Cy: number, + Ri: number, Rm: number, Ro: number, startAngle: number, endAngle: number, + color: string, tickWidth: number) { + if (!tickValues.length) { + return; + } + + const strokeWidth = Ro - Ri; + context.beginPath(); + context.lineWidth = tickWidth; + context.strokeStyle = color; + for (const tick of tickValues) { + const angle = startAngle + tick * endAngle; + const x1 = Cx + (Ri + strokeWidth) * Math.cos(angle); + const y1 = Cy + (Ri + strokeWidth) * Math.sin(angle); + const x2 = Cx + Ri * Math.cos(angle); + const y2 = Cy + Ri * Math.sin(angle); + context.moveTo(x1, y1); + context.lineTo(x2, y2); + } + context.stroke(); +} + +function drawTickBar(context: DigitalGaugeCanvasRenderingContext2D, tickValues: number[], startX: number, startY: number, + distanceBar: number, strokeWidth: number, isVertical: boolean, color: string, tickWidth: number) { + if (!tickValues.length) { + return; + } + + context.beginPath(); + context.lineWidth = tickWidth; + context.strokeStyle = color; + for (const tick of tickValues) { + const tickValue = tick * distanceBar; + if (isVertical) { + context.moveTo(startX - strokeWidth / 2, startY + tickValue - distanceBar); + context.lineTo(startX + strokeWidth / 2, startY + tickValue - distanceBar); + } else { + context.moveTo(startX + tickValue, startY); + context.lineTo(startX + tickValue, startY + strokeWidth); + } + } + context.stroke(); +} + + function drawProgress(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, progress: number) { let neonColor; @@ -917,6 +984,8 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D, drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, true, options.donutStartAngle, options.donutEndAngle); } + drawTickArc(context, options.ticksValue, Cx, Cy, Ri, Rm, Ro, options.donutStartAngle, + options.donutEndAngle - options.donutStartAngle, options.colorTicks, options.tickWidth); } else if (options.gaugeType === 'arc') { if (options.neonGlowBrightness) { context.strokeStyle = neonColor; @@ -927,6 +996,7 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D, if (options.neonGlowBrightness && !options.isMobile) { drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, false); } + drawTickArc(context, options.ticksValue, Cx, Cy, Ri, Rm, Ro, Math.PI, Math.PI, options.colorTicks, options.tickWidth); } else if (options.gaugeType === 'horizontalBar') { if (options.neonGlowBrightness) { context.strokeStyle = neonColor; @@ -940,6 +1010,8 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D, barLeft + (barRight-barLeft)*progress, barTop + strokeWidth/2, neonColor, strokeWidth, false); } + drawTickBar(context, options.ticksValue, barLeft, barTop, barRight - barLeft, strokeWidth, + false, options.colorTicks, options.tickWidth); } else if (options.gaugeType === 'verticalBar') { if (options.neonGlowBrightness) { context.strokeStyle = neonColor; @@ -953,6 +1025,8 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D, baseX + width/2, barBottom - (barBottom-barTop)*progress, neonColor, strokeWidth, true); } + drawTickBar(context, options.ticksValue, baseX + width / 2, barTop, barTop - barBottom, strokeWidth, + true, options.colorTicks, options.tickWidth); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts index 0af0baee96..3c1b7d12c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts @@ -19,25 +19,27 @@ import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge'; import { AnimationRule } from '@home/components/widget/lib/analogue-gauge.models'; import { FontSettings } from '@home/components/widget/lib/settings.models'; -export interface colorLevelProperty { +export interface AttributeSourceProperty { valueSource: string; entityAlias?: string; attribute?: string; value?: number; } -export interface fixedLevelColors { - from?: colorLevelProperty; - to?: colorLevelProperty; +export interface FixedLevelColors { + from?: AttributeSourceProperty; + to?: AttributeSourceProperty; color: string; } -export interface colorLevelSetting { +export interface ColorLevelSetting { value: number; color: string; } -export type colorLevel = Array; +export type colorLevel = Array; + +export type attributesGaugeType = 'levelColors' | 'ticks'; export interface DigitalGaugeSettings { minValue?: number; @@ -60,7 +62,7 @@ export interface DigitalGaugeSettings { gaugeColor?: string; useFixedLevelColor?: boolean; levelColors?: colorLevel; - fixedLevelColors?: fixedLevelColors[]; + fixedLevelColors?: FixedLevelColors[]; animation?: boolean; animationDuration?: number; animationRule?: AnimationRule; @@ -72,6 +74,11 @@ export interface DigitalGaugeSettings { units?: string; hideValue?: boolean; hideMinMax?: boolean; + showTicks?: boolean; + ticksValue?: AttributeSourceProperty[]; + ticks?: number[]; + colorTicks?: string; + tickWidth?: number; } export const digitalGaugeSettingsSchema: JsonSettingsSchema = { @@ -242,6 +249,48 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = { } } }, + showTicks: { + title: 'Show ticks', + type: 'boolean', + default: false + }, + tickWidth: { + title: 'Width ticks', + type: 'number', + default: 4 + }, + colorTicks: { + title: 'Color ticks', + type: 'string', + default: '#666' + }, + ticksValue: { + title: 'The ticks predefined value', + type: 'array', + items: { + title: 'tickValue', + type: 'object', + properties: { + valueSource: { + title: 'Value source', + type: 'string', + default: 'predefinedValue' + }, + entityAlias: { + title: 'Source entity alias', + type: 'string' + }, + attribute: { + title: 'Source entity attribute', + type: 'string' + }, + value: { + title: 'Value (if predefined value is selected)', + type: 'number' + } + } + } + }, animation: { title: 'Enable animation', type: 'boolean', @@ -487,6 +536,40 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = { } ] }, + 'showTicks', + { + key: 'tickWidth', + condition: 'model.showTicks === true' + }, + { + key: 'colorTicks', + condition: 'model.showTicks === true', + type: 'color' + }, + { + key: 'ticksValue', + condition: 'model.showTicks === true', + items: [ + { + key: 'ticksValue[].valueSource', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'predefinedValue', + label: 'Predefined value (Default)' + }, + { + value: 'entityAttribute', + label: 'Value taken from entity attribute' + } + ] + }, + 'ticksValue[].value', + 'ticksValue[].entityAlias', + 'ticksValue[].attribute' + ] + }, 'animation', 'animationDuration', { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts index 0c3dfecaa0..c2372135ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts @@ -17,19 +17,30 @@ import * as CanvasGauges from 'canvas-gauges'; import { WidgetContext } from '@home/models/widget-component.models'; import { - colorLevelSetting, + attributesGaugeType, + AttributeSourceProperty, + ColorLevelSetting, DigitalGaugeSettings, - digitalGaugeSettingsSchema + digitalGaugeSettingsSchema, + FixedLevelColors } from '@home/components/widget/lib/digital-gauge.models'; import * as tinycolor_ from 'tinycolor2'; import { isDefined } from '@core/utils'; import { prepareFontSettings } from '@home/components/widget/lib/settings.models'; import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge'; import { DatePipe } from '@angular/common'; -import {DataKey, Datasource, DatasourceType, JsonSettingsSchema, widgetType} from '@shared/models/widget.models'; +import { + DataKey, + Datasource, + DatasourceData, + DatasourceType, + JsonSettingsSchema, + widgetType +} from '@shared/models/widget.models'; +import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { EMPTY, Observable } from 'rxjs'; import GenericOptions = CanvasGauges.GenericOptions; -import {IWidgetSubscription, WidgetSubscriptionOptions} from "@core/api/widget-api.models"; -import {DataKeyType} from "@shared/models/telemetry/telemetry.models"; const tinycolor = tinycolor_; @@ -37,11 +48,6 @@ const digitalGaugeSettingsSchemaValue = digitalGaugeSettingsSchema; export class TbCanvasDigitalGauge { - private localSettings: DigitalGaugeSettings; - private levelColorsSourcesSubscription: IWidgetSubscription; - - private gauge: CanvasDigitalGauge; - static get settingsSchema(): JsonSettingsSchema { return digitalGaugeSettingsSchemaValue; } @@ -84,6 +90,12 @@ export class TbCanvasDigitalGauge { this.localSettings.fixedLevelColors = settings.fixedLevelColors || []; } + this.localSettings.showTicks = settings.showTicks || false; + this.localSettings.ticks = []; + this.localSettings.ticksValue = settings.ticksValue || []; + this.localSettings.tickWidth = settings.tickWidth || 4; + this.localSettings.colorTicks = settings.colorTicks || '#666'; + this.localSettings.decimals = isDefined(dataKey.decimals) ? dataKey.decimals : ((isDefined(settings.decimals) && settings.decimals !== null) ? settings.decimals : ctx.decimals); @@ -137,6 +149,10 @@ export class TbCanvasDigitalGauge { gaugeColor: this.localSettings.gaugeColor, levelColors: this.localSettings.levelColors, + colorTicks: this.localSettings.colorTicks, + tickWidth: this.localSettings.tickWidth, + ticks: this.localSettings.ticks, + title: this.localSettings.title, fontTitleSize: this.localSettings.titleFont.size, @@ -192,76 +208,92 @@ export class TbCanvasDigitalGauge { this.init(); } + private localSettings: DigitalGaugeSettings; + private levelColorsSourcesSubscription: IWidgetSubscription; + private ticksSourcesSubscription: IWidgetSubscription; + + private gauge: CanvasDigitalGauge; + + static generateDatasource(ctx: WidgetContext, datasources: Datasource[], entityAlias: string, + attribute: string, settings: any): Datasource[]{ + const entityAliasId = ctx.aliasController.getEntityAliasId(entityAlias); + if (!entityAliasId) { + throw new Error('Not valid entity aliase name ' + entityAlias); + } + + const datasource = datasources.find((datasourceIteration) => { + return datasourceIteration.entityAliasId === entityAliasId; + }); + + const dataKey: DataKey = { + type: DataKeyType.attribute, + name: attribute, + label: attribute, + settings: [settings], + _hash: Math.random() + }; + + if (datasource) { + const findDataKey = datasource.dataKeys.find((dataKeyIteration) => { + return dataKeyIteration.name === attribute; + }); + + if (findDataKey) { + findDataKey.settings.push(settings); + } else { + datasource.dataKeys.push(dataKey) + } + } else { + const datasourceAttribute: Datasource = { + type: DatasourceType.entity, + name: entityAlias, + aliasName: entityAlias, + entityAliasId, + dataKeys: [dataKey] + }; + datasources.push(datasourceAttribute); + } + + return datasources; + } + init() { if (this.localSettings.useFixedLevelColor) { if (this.localSettings.fixedLevelColors && this.localSettings.fixedLevelColors.length > 0) { this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors); - this.updateLevelColors(this.localSettings.levelColors); } + + if (this.localSettings.showTicks) { + if (this.localSettings.ticksValue && this.localSettings.ticksValue.length) { + this.localSettings.ticks = this.settingTicksSubscribe(this.localSettings.ticksValue); + } + } + this.updateSetting(); } } - settingLevelColorsSubscribe(options) { + settingLevelColorsSubscribe(options: FixedLevelColors[]): ColorLevelSetting[] { let levelColorsDatasource: Datasource[] = []; - let predefineLevelColors: colorLevelSetting[] = []; + const predefineLevelColors: ColorLevelSetting[] = []; - function setLevelColor(levelSetting, color) { + function setLevelColor(levelSetting: AttributeSourceProperty, color: string) { if (levelSetting.valueSource === 'predefinedValue' && isFinite(levelSetting.value)) { predefineLevelColors.push({ value: levelSetting.value, - color: color + color }) } else if (levelSetting.entityAlias && levelSetting.attribute) { - let entityAliasId = this.ctx.aliasController.getEntityAliasId(levelSetting.entityAlias); - if (!entityAliasId) { + try { + levelColorsDatasource = TbCanvasDigitalGauge.generateDatasource(this.ctx, levelColorsDatasource, + levelSetting.entityAlias, levelSetting.attribute, {color, index: predefineLevelColors.length}); + } catch (e) { return; } - - let datasource = levelColorsDatasource.find((datasource) => { - return datasource.entityAliasId === entityAliasId; - }); - - let dataKey: DataKey = { - type: DataKeyType.attribute, - name: levelSetting.attribute, - label: levelSetting.attribute, - settings: [{ - color: color, - index: predefineLevelColors.length - }], - _hash: Math.random() - }; - - if (datasource) { - let findDataKey = datasource.dataKeys.find((dataKey) => { - return dataKey.name === levelSetting.attribute; - }); - - if (findDataKey) { - findDataKey.settings.push({ - color: color, - index: predefineLevelColors.length - }); - } else { - datasource.dataKeys.push(dataKey) - } - } else { - let datasource: Datasource = { - type: DatasourceType.entity, - name: levelSetting.entityAlias, - aliasName: levelSetting.entityAlias, - entityAliasId: entityAliasId, - dataKeys: [dataKey] - }; - levelColorsDatasource.push(datasource); - } - predefineLevelColors.push(null); } } - for (let i = 0; i < options.length; i++) { - let levelColor = options[i]; + for(const levelColor of options){ if (levelColor.from) { setLevelColor.call(this, levelColor.from, levelColor.color); } @@ -270,49 +302,86 @@ export class TbCanvasDigitalGauge { } } - this.subscribeLevelColorsAttributes(levelColorsDatasource); + this.subscribeAttributes(levelColorsDatasource, 'levelColors').subscribe((subscription) => { + this.levelColorsSourcesSubscription = subscription; + }); return predefineLevelColors; } - updateLevelColors(levelColors) { - (this.gauge.options as CanvasDigitalGaugeOptions).levelColors = levelColors; - this.gauge.options = CanvasDigitalGauge.configure(this.gauge.options); - this.gauge.update({} as CanvasDigitalGaugeOptions); + settingTicksSubscribe(options: AttributeSourceProperty[]): number[] { + let ticksDatasource: Datasource[] = []; + const predefineTicks: number[] = []; + + for(const tick of options){ + if (tick.valueSource === 'predefinedValue' && isFinite(tick.value)) { + predefineTicks.push(tick.value) + } else if (tick.entityAlias && tick.attribute) { + try { + ticksDatasource = TbCanvasDigitalGauge + .generateDatasource(this.ctx, ticksDatasource, tick.entityAlias, tick.attribute, predefineTicks.length); + } catch (e) { + continue; + } + predefineTicks.push(null); + } + } + + this.subscribeAttributes(ticksDatasource, 'ticks').subscribe((subscription) => { + this.ticksSourcesSubscription = subscription; + }); + + return predefineTicks; } - subscribeLevelColorsAttributes(datasources: Datasource[]) { - let TbCanvasDigitalGauge = this; - let levelColorsSourcesSubscriptionOptions: WidgetSubscriptionOptions = { - datasources: datasources, + subscribeAttributes(datasource: Datasource[], typeAttributes: attributesGaugeType): Observable { + if (!datasource.length) { + return EMPTY; + } + + const levelColorsSourcesSubscriptionOptions: WidgetSubscriptionOptions = { + datasources: datasource, useDashboardTimewindow: false, type: widgetType.latest, callbacks: { onDataUpdated: (subscription) => { - for (let i = 0; i < subscription.data.length; i++) { - let keyData = subscription.data[i]; - if (keyData && keyData.data && keyData.data[0]) { - let attrValue = keyData.data[0][1]; - if (isFinite(attrValue)) { - for (let i = 0; i < keyData.dataKey.settings.length; i++) { - let setting = keyData.dataKey.settings[i]; - this.localSettings.levelColors[setting.index] = { - value: attrValue, - color: setting.color - }; - } - } - } - } - this.updateLevelColors(this.localSettings.levelColors); + this.updateAttribute(subscription.data, typeAttributes); } } }; - this.ctx.subscriptionApi.createSubscription(levelColorsSourcesSubscriptionOptions, true).subscribe( - (subscription) => { - TbCanvasDigitalGauge.levelColorsSourcesSubscription = subscription; + + return this.ctx.subscriptionApi.createSubscription(levelColorsSourcesSubscriptionOptions, true); + } + + updateAttribute(data: Array, typeAttributes: attributesGaugeType) { + for (const keyData of data) { + if (keyData && keyData.data && keyData.data[0]) { + const attrValue = keyData.data[0][1]; + if (isFinite(attrValue)) { + for (const setting of keyData.dataKey.settings) { + switch (typeAttributes) { + case 'levelColors': + this.localSettings.levelColors[setting.index] = { + value: attrValue, + color: setting.color + }; + break; + case 'ticks': + this.localSettings.ticks[setting] = attrValue; + break; + } + } + } } - ); + } + this.updateSetting(); + } + + updateSetting() { + (this.gauge.options as CanvasDigitalGaugeOptions).ticks = this.localSettings.ticks; + (this.gauge.options as CanvasDigitalGaugeOptions).levelColors = this.localSettings.levelColors; + this.gauge.options = CanvasDigitalGauge.configure(this.gauge.options); + this.gauge.update({} as CanvasDigitalGaugeOptions); } update() {