diff --git a/.betterer.results b/.betterer.results index cc1be080..cdce8379 100644 --- a/.betterer.results +++ b/.betterer.results @@ -181,39 +181,7 @@ exports[`strictNullChecks`] = { [156, 4, 20, "Object is possibly \'undefined\'.", "2522653710"], [157, 4, 20, "Object is possibly \'undefined\'.", "2522653710"] ], - "src/app/widgets/gauge-steel/gauge-steel.component.ts:3394617356": [ - [74, 38, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'string\'.", "2620553983"], - [75, 35, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'string\'.", "2620553983"], - [76, 37, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'boolean\'.", "2620553983"], - [77, 38, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'string\'.", "2620553983"], - [78, 43, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'string\'.", "2620553983"], - [79, 38, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'string\'.", "2620553983"], - [80, 36, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'number\'.", "2620553983"], - [81, 36, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'number\'.", "2620553983"], - [82, 36, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'number\'.", "2620553983"], - [83, 36, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'ISkZone[]\'.", "2620553983"], - [84, 33, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'string\'.", "2620553983"], - [85, 33, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'string\'.", "2620553983"], - [86, 33, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'number\'.", "2620553983"], - [127, 52, 7, "\'a.lower\' is possibly \'undefined\'.", "2967727561"], - [127, 62, 7, "\'b.lower\' is possibly \'undefined\'.", "4173952970"], - [130, 12, 5, "Type \'null\' is not assignable to type \'number\'.", "173263814"], - [131, 12, 5, "Type \'null\' is not assignable to type \'number\'.", "183473191"], - [136, 20, 12, "Object is possibly \'undefined\'.", "765689629"], - [139, 20, 12, "Object is possibly \'undefined\'.", "765689629"], - [142, 20, 12, "Object is possibly \'undefined\'.", "765689629"], - [145, 20, 12, "Object is possibly \'undefined\'.", "765689629"], - [148, 20, 12, "Object is possibly \'undefined\'.", "765689629"], - [157, 10, 5, "Type \'number | undefined\' is not assignable to type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "173263814"], - [158, 10, 5, "Type \'number | undefined\' is not assignable to type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "183473191"], - [160, 10, 5, "Type \'number | null\' is not assignable to type \'number\'.\\n Type \'null\' is not assignable to type \'number\'.", "173263814"], - [160, 57, 10, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "467412886"], - [161, 10, 5, "Type \'number | null\' is not assignable to type \'number\'.\\n Type \'null\' is not assignable to type \'number\'.", "183473191"], - [161, 57, 10, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "440544247"], - [179, 24, 40, "Argument of type \'any\' is not assignable to parameter of type \'never\'.", "1277859535"], - [183, 22, 40, "Argument of type \'any\' is not assignable to parameter of type \'never\'.", "1277859535"] - ], - "src/app/widgets/minichart/minichart.component.ts:15445999": [ + "src/app/widgets/minichart/minichart.component.ts:765110486": [ [43, 9, 5, "Type \'null\' is not assignable to type \'string\'.", "176948952"], [44, 9, 8, "Type \'null\' is not assignable to type \'string\'.", "806402680"], [45, 9, 10, "Type \'null\' is not assignable to type \'string\'.", "1967398792"], @@ -248,86 +216,6 @@ exports[`strictNullChecks`] = { [546, 4, 14, "Type \'null\' is not assignable to type \'Subscription\'.", "2516429621"], [558, 4, 6, "Type \'({ x: number | null; y: number; } | { x: number; y: number | null; })[]\' is not assignable to type \'IDataSetRow[]\'.\\n Type \'{ x: number | null; y: number; } | { x: number; y: number | null; }\' is not assignable to type \'IDataSetRow\'.\\n Type \'{ x: number | null; y: number; }\' is not assignable to type \'IDataSetRow\'.\\n Types of property \'x\' are incompatible.\\n Type \'number | null\' is not assignable to type \'number\'.\\n Type \'null\' is not assignable to type \'number\'.", "2123913871"] ], - "src/app/widgets/svg-boolean-button/svg-boolean-button.component.ts:1543445686": [ - [14, 41, 4, "No overload matches this call.\\n Overload 1 of 5, \'(initialValue: IDynamicControl, opts?: InputOptionsWithoutTransform | undefined): InputSignal<...>\', gave the following error.\\n Argument of type \'null\' is not assignable to parameter of type \'IDynamicControl\'.\\n Overload 2 of 5, \'(initialValue: undefined, opts: InputOptionsWithoutTransform): InputSignal\', gave the following error.\\n Argument of type \'null\' is not assignable to parameter of type \'undefined\'.", "2087897566"], - [15, 33, 4, "Argument of type \'null\' is not assignable to parameter of type \'ITheme\'.", "2087897566"], - [21, 10, 8, "Type \'null\' is not assignable to type \'ITheme\'.", "2459887219"], - [114, 8, 22, "Type \'\\"black\\"\' is not assignable to type \'null\'.", "807528371"], - [115, 8, 23, "Type \'string\' is not assignable to type \'null\'.", "2686791206"], - [116, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [119, 8, 22, "Type \'string\' is not assignable to type \'null\'.", "807528371"], - [120, 8, 23, "Type \'string\' is not assignable to type \'null\'.", "2686791206"], - [121, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [124, 8, 22, "Type \'string\' is not assignable to type \'null\'.", "807528371"], - [125, 8, 23, "Type \'string\' is not assignable to type \'null\'.", "2686791206"], - [126, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [129, 8, 22, "Type \'string\' is not assignable to type \'null\'.", "807528371"], - [130, 8, 23, "Type \'string\' is not assignable to type \'null\'.", "2686791206"], - [131, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [134, 8, 22, "Type \'string\' is not assignable to type \'null\'.", "807528371"], - [135, 8, 23, "Type \'string\' is not assignable to type \'null\'.", "2686791206"], - [136, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [139, 8, 22, "Type \'string\' is not assignable to type \'null\'.", "807528371"], - [140, 8, 23, "Type \'string\' is not assignable to type \'null\'.", "2686791206"], - [141, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [144, 8, 22, "Type \'string\' is not assignable to type \'null\'.", "807528371"], - [145, 8, 23, "Type \'string\' is not assignable to type \'null\'.", "2686791206"], - [146, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [149, 8, 22, "Type \'string\' is not assignable to type \'null\'.", "807528371"], - [150, 8, 23, "Type \'string\' is not assignable to type \'null\'.", "2686791206"], - [151, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [154, 8, 22, "Type \'\\"black\\"\' is not assignable to type \'null\'.", "807528371"], - [155, 8, 23, "Type \'string\' is not assignable to type \'null\'.", "2686791206"], - [156, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"] - ], - "src/app/widgets/svg-boolean-light/svg-boolean-light.component.ts:520976916": [ - [15, 41, 4, "No overload matches this call.\\n Overload 1 of 5, \'(initialValue: IDynamicControl, opts?: InputOptionsWithoutTransform | undefined): InputSignal<...>\', gave the following error.\\n Argument of type \'null\' is not assignable to parameter of type \'IDynamicControl\'.\\n Overload 2 of 5, \'(initialValue: undefined, opts: InputOptionsWithoutTransform): InputSignal\', gave the following error.\\n Argument of type \'null\' is not assignable to parameter of type \'undefined\'.", "2087897566"], - [16, 33, 4, "Argument of type \'null\' is not assignable to parameter of type \'ITheme\'.", "2087897566"], - [22, 10, 9, "Type \'null\' is not assignable to type \'boolean\'.", "2931300859"], - [24, 10, 8, "Type \'null\' is not assignable to type \'ITheme\'.", "2459887219"], - [59, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [60, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [63, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [64, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [67, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [68, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [71, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [72, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [75, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [76, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [79, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [80, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [83, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [84, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [87, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [88, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [91, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [92, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"] - ], - "src/app/widgets/svg-boolean-switch/svg-boolean-switch.component.ts:1706226148": [ - [13, 41, 4, "No overload matches this call.\\n Overload 1 of 5, \'(initialValue: IDynamicControl, opts?: InputOptionsWithoutTransform | undefined): InputSignal<...>\', gave the following error.\\n Argument of type \'null\' is not assignable to parameter of type \'IDynamicControl\'.\\n Overload 2 of 5, \'(initialValue: undefined, opts: InputOptionsWithoutTransform): InputSignal\', gave the following error.\\n Argument of type \'null\' is not assignable to parameter of type \'undefined\'.", "2087897566"], - [14, 33, 4, "Argument of type \'null\' is not assignable to parameter of type \'ITheme\'.", "2087897566"], - [20, 10, 9, "Type \'null\' is not assignable to type \'boolean\'.", "2931300859"], - [21, 10, 8, "Type \'null\' is not assignable to type \'ITheme\'.", "2459887219"], - [75, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [76, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [79, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [80, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [83, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [84, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [87, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [88, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [91, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [92, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [95, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [96, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [99, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [100, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [103, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [104, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"], - [107, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "2145130966"], - [108, 8, 15, "Type \'string\' is not assignable to type \'null\'.", "1525246139"] - ], "src/app/widgets/svg-racesteer/svg-racesteer.component.ts:3776013780": [ [36, 46, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'number\'.", "2620553983"], [37, 47, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'number\'.", "2620553983"], @@ -629,67 +517,6 @@ exports[`strictNullChecks`] = { "src/app/widgets/widget-gauge-ng-compass/widget-gauge-ng-compass.component.ts:1976286112": [ [235, 30, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"] ], - "src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.ts:1738357127": [ - [93, 43, 7, "Argument of type \'() => IScale | { min: number | undefined; max: number | undefined; majorTicks: never[]; }\' is not assignable to parameter of type \'() => IScale\'.\\n Type \'IScale | { min: number | undefined; max: number | undefined; majorTicks: never[]; }\' is not assignable to type \'IScale\'.\\n Type \'{ min: number | undefined; max: number | undefined; majorTicks: never[]; }\' is not assignable to type \'IScale\'.\\n Types of property \'min\' are incompatible.\\n Type \'number | undefined\' is not assignable to type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "1860344028"], - [97, 44, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [97, 44, 22, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "4184355206"], - [97, 68, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [99, 18, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [99, 47, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [126, 25, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [129, 49, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [129, 49, 22, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "4184355206"], - [129, 74, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [129, 74, 22, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "4194286055"], - [157, 78, 22, "Object is possibly \'undefined\'.", "576852750"], - [170, 31, 22, "Argument of type \'IWidgetSvcConfig | undefined\' is not assignable to parameter of type \'IWidgetSvcConfig\'.\\n Type \'undefined\' is not assignable to type \'IWidgetSvcConfig\'.", "576852750"], - [201, 12, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [204, 29, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [205, 34, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [205, 34, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [209, 32, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [210, 35, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [212, 37, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [213, 35, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [218, 32, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [219, 35, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [221, 37, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [222, 35, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [227, 32, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [228, 35, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [230, 37, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [231, 35, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [277, 30, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"] - ], - "src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.ts:1445903677": [ - [176, 78, 22, "Object is possibly \'undefined\'.", "576852750"], - [189, 31, 22, "Argument of type \'IWidgetSvcConfig | undefined\' is not assignable to parameter of type \'IWidgetSvcConfig\'.\\n Type \'undefined\' is not assignable to type \'IWidgetSvcConfig\'.", "576852750"], - [219, 12, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [224, 39, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [225, 38, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [226, 36, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [229, 39, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [230, 38, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [231, 36, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [234, 39, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [235, 38, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [236, 36, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [239, 39, 5, "\'theme\' is possibly \'null\'.", "183206964"], - [240, 38, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [240, 85, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [240, 85, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [240, 121, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [240, 121, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [241, 46, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [241, 46, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [308, 81, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [309, 30, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [309, 84, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [313, 101, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [323, 54, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [331, 17, 9, "\'cfg.gauge\' is possibly \'undefined\'.", "4124321432"], - [333, 10, 9, "\'cfg.gauge\' is possibly \'undefined\'.", "4124321432"] - ], "src/app/widgets/widget-iframe/widget-iframe.component.ts:2602616758": [ [30, 4, 9, "Type \'null\' is not assignable to type \'string | undefined\'.", "2966443234"] ], @@ -700,64 +527,6 @@ exports[`strictNullChecks`] = { [50, 4, 5, "Type \'{ controlPath: { description: string; path: null; source: null; pathType: \\"multiple\\"; supportsPut: true; isPathConfigurable: true; pathRequired: true; showPathSkUnitsFilter: false; pathSkUnitsFilter: null; showConvertUnitTo: false; convertUnitTo: null; sampleTime: number; }; }\' is not assignable to type \'IPathArray | IWidgetPath[] | undefined\'.\\n The types of \'controlPath.convertUnitTo\' are incompatible between these types.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "187670523"], [160, 22, 10, "Object is possibly \'undefined\'.", "3996575854"] ], - "src/app/widgets/widget-numeric/widget-numeric.component.ts:1149234437": [ - [62, 40, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'string\'.", "2620553983"], - [70, 10, 9, "Type \'null\' is not assignable to type \'number\'.", "834508926"], - [71, 10, 8, "Type \'null\' is not assignable to type \'number\'.", "955872026"], - [72, 10, 8, "Type \'null\' is not assignable to type \'number\'.", "3919488324"], - [73, 10, 10, "Type \'undefined\' is not assignable to type \'string\'.", "4110290259"], - [74, 10, 15, "Type \'undefined\' is not assignable to type \'string\'.", "117091620"], - [107, 9, 23, "Object is possibly \'undefined\'.", "924630673"], - [112, 35, 12, "Object is possibly \'null\'.", "765689629"], - [115, 35, 12, "Object is possibly \'null\'.", "765689629"], - [118, 35, 12, "Object is possibly \'null\'.", "765689629"], - [130, 27, 22, "Object is possibly \'undefined\'.", "576852750"], - [130, 27, 36, "Argument of type \'boolean | undefined\' is not assignable to parameter of type \'boolean\'.\\n Type \'undefined\' is not assignable to type \'boolean\'.", "302325484"], - [154, 8, 13, "Type \'null\' is not assignable to type \'number\'.", "2207342764"], - [155, 8, 13, "Type \'null\' is not assignable to type \'number\'.", "1610969202"], - [156, 8, 14, "Type \'null\' is not assignable to type \'number\'.", "261643286"], - [189, 6, 16, "Object is possibly \'undefined\'.", "1024658627"], - [196, 4, 14, "Type \'CanvasRenderingContext2D | null\' is not assignable to type \'CanvasRenderingContext2D\'.\\n Type \'null\' is not assignable to type \'CanvasRenderingContext2D\'.", "912271434"], - [228, 29, 22, "Object is possibly \'undefined\'.", "576852750"], - [233, 21, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [233, 21, 9, "\'cfg.paths\' is possibly \'undefined\'.", "4139429943"], - [234, 4, 16, "Object is possibly \'undefined\'.", "1024658627"], - [235, 4, 16, "Object is possibly \'undefined\'.", "1024658627"], - [236, 4, 16, "Object is possibly \'undefined\'.", "1024658627"], - [236, 4, 22, "Type \'string | undefined\' is not assignable to type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "2885375952"], - [236, 29, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [237, 4, 16, "Object is possibly \'undefined\'.", "1024658627"], - [238, 4, 16, "Object is possibly \'undefined\'.", "1024658627"], - [238, 4, 27, "Type \'number | undefined\' is not assignable to type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "301849712"], - [238, 34, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [239, 4, 16, "Object is possibly \'undefined\'.", "1024658627"], - [239, 4, 26, "Type \'number | undefined\' is not assignable to type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "1509249862"], - [239, 33, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [240, 4, 16, "Object is possibly \'undefined\'.", "1024658627"], - [240, 4, 26, "Type \'number | undefined\' is not assignable to type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "1509249624"], - [240, 33, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [241, 4, 16, "Object is possibly \'undefined\'.", "1024658627"], - [241, 4, 29, "Type \'boolean | undefined\' is not assignable to type \'boolean\'.\\n Type \'undefined\' is not assignable to type \'boolean\'.", "763700999"], - [241, 36, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [242, 4, 16, "Object is possibly \'undefined\'.", "1024658627"], - [242, 4, 30, "Type \'boolean | undefined\' is not assignable to type \'null\'.\\n Type \'undefined\' is not assignable to type \'null\'.", "1638858803"], - [242, 37, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [248, 34, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [249, 55, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [258, 17, 9, "\'cfg.paths\' is possibly \'undefined\'.", "4139429943"], - [274, 12, 15, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4102914388"], - [327, 18, 22, "Object is possibly \'undefined\'.", "576852750"], - [327, 18, 28, "Object is possibly \'undefined\'.", "3500113022"], - [331, 56, 22, "Object is possibly \'undefined\'.", "576852750"], - [336, 9, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [336, 25, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [338, 8, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [339, 95, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [341, 8, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [342, 96, 3, "\'cfg\' is possibly \'undefined\'.", "193415815"], - [362, 12, 22, "Object is possibly \'undefined\'.", "576852750"], - [362, 12, 28, "Object is possibly \'undefined\'.", "3500113022"] - ], "src/app/widgets/widget-position/widget-position.component.ts:1704530161": [ [91, 34, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"] ], @@ -783,61 +552,6 @@ exports[`strictNullChecks`] = { [215, 29, 1, "Argument of type \'number | null\' is not assignable to parameter of type \'number\'.\\n Type \'null\' is not assignable to type \'number\'.", "177619"], [336, 100, 27, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "1820305006"] ], - "src/app/widgets/widget-simple-linear/widget-simple-linear.component.ts:1973487785": [ - [60, 48, 4, "Argument of type \'null\' is not assignable to parameter of type \'number\'.", "2087897566"], - [74, 17, 9, "\'cfg.paths\' is possibly \'undefined\'.", "4139429943"], - [75, 16, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [76, 16, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [77, 64, 3, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "193413807"], - [94, 31, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [94, 31, 22, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "4184355206"], - [97, 51, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [97, 51, 22, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "4184355206"], - [97, 76, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [97, 76, 22, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "4194286055"], - [111, 53, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [118, 21, 9, "\'cfg.paths\' is possibly \'undefined\'.", "4139429943"], - [119, 28, 9, "\'cfg.gauge\' is possibly \'undefined\'.", "4124321432"], - [130, 34, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [155, 22, 13, "Object is possibly \'null\'.", "1985302592"] - ], - "src/app/widgets/widget-slider/widget-slider.component.ts:413411294": [ - [28, 4, 5, "Type \'{ gaugePath: { description: string; path: null; source: null; pathType: \\"number\\"; isPathConfigurable: true; showPathSkUnitsFilter: false; pathSkUnitsFilter: null; showConvertUnitTo: false; convertUnitTo: null; supportsPut: true; sampleTime: number; }; }\' is not assignable to type \'IPathArray | IWidgetPath[] | undefined\'.\\n The types of \'\'gaugePath\'.convertUnitTo\' are incompatible between these types.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "187670523"], - [59, 40, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'string\'.", "2620553983"], - [60, 38, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'string\'.", "2620553983"], - [88, 23, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [102, 83, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [102, 83, 22, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "4184355206"], - [115, 21, 9, "Argument of type \'string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "4119212820"], - [124, 41, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [124, 41, 22, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "4184355206"], - [125, 19, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [126, 48, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [126, 48, 22, "Argument of type \'number | undefined\' is not assignable to parameter of type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "4194286055"], - [127, 19, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [142, 23, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [142, 23, 22, "\'cfg.displayScale.upper\' is possibly \'undefined\'.", "4194286055"], - [142, 48, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [142, 48, 22, "\'cfg.displayScale.lower\' is possibly \'undefined\'.", "4184355206"], - [144, 21, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [144, 21, 22, "\'cfg.displayScale.lower\' is possibly \'undefined\'.", "4184355206"], - [150, 23, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [150, 23, 22, "\'cfg.displayScale.upper\' is possibly \'undefined\'.", "4194286055"], - [150, 48, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [150, 48, 22, "\'cfg.displayScale.lower\' is possibly \'undefined\'.", "4184355206"], - [152, 69, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [152, 69, 22, "\'cfg.displayScale.lower\' is possibly \'undefined\'.", "4184355206"], - [156, 20, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [156, 20, 22, "\'cfg.displayScale.upper\' is possibly \'undefined\'.", "4194286055"], - [156, 45, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [156, 45, 22, "\'cfg.displayScale.lower\' is possibly \'undefined\'.", "4184355206"], - [218, 10, 14, "Type \'number | undefined\' is not assignable to type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "425845355"], - [218, 27, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [220, 10, 14, "Type \'number | undefined\' is not assignable to type \'number\'.\\n Type \'undefined\' is not assignable to type \'number\'.", "425845355"], - [220, 27, 16, "\'cfg.displayScale\' is possibly \'undefined\'.", "2211049739"], - [251, 24, 6, "\'colors\' is possibly \'undefined\'.", "1544348075"], - [252, 22, 6, "\'colors\' is possibly \'undefined\'.", "1544348075"] - ], "src/app/widgets/widget-text/widget-text.component.ts:2471009854": [ [51, 40, 9, "Argument of type \'undefined\' is not assignable to parameter of type \'string\'.", "2620553983"], [52, 10, 10, "Type \'undefined\' is not assignable to type \'string\'.", "4110290259"], diff --git a/src/app/core/services/data.service.spec.ts b/src/app/core/services/data.service.spec.ts index e0a3195a..e7b60bed 100644 --- a/src/app/core/services/data.service.spec.ts +++ b/src/app/core/services/data.service.spec.ts @@ -389,4 +389,41 @@ describe('DataService', () => { expect(service.getPathObject('self.navigation.anchor.position')!.type).toBeUndefined(); }); + + it('files a context-less delta value under self (empty context assumes self)', () => { + const values: unknown[] = []; + service + .subscribePath('self.navigation.speedOverGround', 'default') + .subscribe(update => values.push(update.data.value)); + + // A delta with no context must land on the self root, not an "undefined." key (#209). + dataPathUpdates$.next({ + context: undefined, + path: 'navigation.speedOverGround', + source: 'default', + timestamp: '2026-01-01T00:00:01.000Z', + value: 3.2, + }); + + expect(values.at(-1)).toBe(3.2); + }); + + it('keeps a foreign-context value under its own root, not self', () => { + const selfValues: unknown[] = []; + const foreignValues: unknown[] = []; + service.subscribePath('self.navigation.speedOverGround', 'default').subscribe(u => selfValues.push(u.data.value)); + service.subscribePath('vessels.abc.navigation.speedOverGround', 'default').subscribe(u => foreignValues.push(u.data.value)); + + dataPathUpdates$.next({ + context: 'vessels.abc', + path: 'navigation.speedOverGround', + source: 'default', + timestamp: '2026-01-01T00:00:01.000Z', + value: 7, + }); + + expect(foreignValues.at(-1)).toBe(7); + // Self stays at its initial null emission; the foreign value never leaks onto it. + expect(selfValues).not.toContain(7); + }); }); diff --git a/src/app/core/services/data.service.ts b/src/app/core/services/data.service.ts index 274c8622..d2f5300f 100644 --- a/src/app/core/services/data.service.ts +++ b/src/app/core/services/data.service.ts @@ -612,8 +612,12 @@ export class DataService implements OnDestroy { } private setPathContext(context: string | undefined, path: string): string { - const finalPath = context !== this._selfUrn ? `${context}.${path}` : `${SELFROOTDEF}.${path}`; - return finalPath; + // An empty/absent context means "self" (per the ISkPathData/IMeta contract), same as a + // context that already equals the learned self URN. Anything else is a foreign root node. + if (!context || context === this._selfUrn) { + return `${SELFROOTDEF}.${path}`; + } + return `${context}.${path}`; } /** diff --git a/src/app/widgets/gauge-steel/gauge-steel.component.spec.ts b/src/app/widgets/gauge-steel/gauge-steel.component.spec.ts index 1daa6c15..46fdc4f5 100644 --- a/src/app/widgets/gauge-steel/gauge-steel.component.spec.ts +++ b/src/app/widgets/gauge-steel/gauge-steel.component.spec.ts @@ -1,7 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { GaugeSteelComponent } from './gauge-steel.component'; import { UnitsService } from '../../core/services/units.service'; +import { States } from '../../core/interfaces/signalk-interfaces'; describe('GaugeSteelComponent', () => { let component: GaugeSteelComponent; @@ -31,4 +32,22 @@ describe('GaugeSteelComponent', () => { it('should be created', () => { expect(component).toBeTruthy(); }); + + it('renders an open-ended low-alarm zone as a band clamped to the gauge minimum', () => { + const sectionSpy = vi.fn((lower: number, upper: number, color: string) => ({ lower, upper, color })); + (globalThis as unknown as { steelseries: Record }).steelseries.Section = sectionSpy; + + fixture.componentRef.setInput('minValue', 10); + fixture.componentRef.setInput('maxValue', 15); + fixture.componentRef.setInput('units', 'V'); + fixture.componentRef.setInput('themeColors', { zoneAlarm: 'red' }); + // Low alarm with no lower bound — "alarm below 11.5", the common open-ended zone. + fixture.componentRef.setInput('zones', [{ upper: 11.5, state: States.Alarm }]); + + (component as unknown as { buildOptions: () => void }).buildOptions(); + + // The unset lower bound clamps to the gauge minimum (10) instead of producing a + // NaN section that does not draw; the upper is converted (identity mock -> 11.5). + expect(sectionSpy).toHaveBeenCalledWith(10, 11.5, 'red'); + }); }); diff --git a/src/app/widgets/gauge-steel/gauge-steel.component.ts b/src/app/widgets/gauge-steel/gauge-steel.component.ts index 94735f52..054dc39f 100644 --- a/src/app/widgets/gauge-steel/gauge-steel.component.ts +++ b/src/app/widgets/gauge-steel/gauge-steel.component.ts @@ -72,21 +72,21 @@ export class GaugeSteelComponent implements OnInit, OnChanges, OnDestroy { private readonly canvasService = inject(CanvasService); protected readonly canvasRef = viewChild>('gaugeCanvas'); - readonly widgetUUID = input(undefined); - readonly subType = input(undefined); // linear or radial - readonly barGauge = input(undefined); - readonly radialSize = input(undefined); - readonly backgroundColor = input(undefined); - readonly frameColor = input(undefined); - readonly minValue = input(undefined); - readonly maxValue = input(undefined); - readonly decimals = input(undefined); - readonly zones = input(undefined); - readonly title = input(undefined); - readonly units = input(undefined); - readonly value = input(undefined); + readonly widgetUUID = input(); + readonly subType = input(); // linear or radial + readonly barGauge = input(); + readonly radialSize = input(); + readonly backgroundColor = input(); + readonly frameColor = input(); + readonly minValue = input(); + readonly maxValue = input(); + readonly decimals = input(); + readonly zones = input(); + readonly title = input(); + readonly units = input(); + readonly value = input(); // eslint-disable-next-line @angular-eslint/no-input-rename - readonly theme = input(undefined, { alias: "themeColors" }); + readonly theme = input(null, { alias: "themeColors" }); private gaugeStarted = false; private gauge; @@ -121,61 +121,64 @@ export class GaugeSteelComponent implements OnInit, OnChanges, OnDestroy { const zones = this.zones(); if (zones) { - const sections = []; - const areas = []; + const sections: unknown[] = []; + const areas: unknown[] = []; // Sort zones based on lower value - const sortedZones = [...zones].sort((a, b) => a.lower - b.lower); + const sortedZones = [...zones].sort((a, b) => (a.lower ?? -Infinity) - (b.lower ?? -Infinity)); for (const zone of sortedZones) { - let lower: number = null; - let upper: number = null; + let lower: number | null | undefined = null; + let upper: number | null | undefined = null; + // Zones still draw before the theme loads (transparent fill), so the gauge + // shows its zone geometry rather than skipping the render as the other widgets do. let color: string; switch (zone.state) { case States.Emergency: - color = this.theme().zoneEmergency; + color = this.theme()?.zoneEmergency ?? "rgba(0,0,0,0)"; break; case States.Alarm: - color = this.theme().zoneAlarm; + color = this.theme()?.zoneAlarm ?? "rgba(0,0,0,0)"; break; case States.Warn: - color = this.theme().zoneWarn; + color = this.theme()?.zoneWarn ?? "rgba(0,0,0,0)"; break; case States.Alert: - color = this.theme().zoneAlert; + color = this.theme()?.zoneAlert ?? "rgba(0,0,0,0)"; break; case States.Nominal: - color = this.theme().zoneNominal; + color = this.theme()?.zoneNominal ?? "rgba(0,0,0,0)"; break; default: color = "rgba(0,0,0,0)"; } // Perform Units conversions on zone range - const units = this.units(); + const units = this.units() ?? ''; if (units == "ratio") { lower = zone.lower; upper = zone.upper; } else { - lower = this.unitsService.convertToUnit(units, zone.lower); - upper = this.unitsService.convertToUnit(units, zone.upper); + lower = zone.lower !== undefined ? this.unitsService.convertToUnit(units, zone.lower) : undefined; + upper = zone.upper !== undefined ? this.unitsService.convertToUnit(units, zone.upper) : undefined; } // Skip zones that are completely outside the gauge range - if (upper < this.minValue() || lower > this.maxValue()) { + // (an unset bound means "unbounded" on that side, so it never triggers the skip) + if ((upper ?? Infinity) < (this.minValue() ?? -Infinity) || (lower ?? -Infinity) > (this.maxValue() ?? Infinity)) { continue; } - // If lower or upper are null, set them to minValue or maxValue - lower = lower !== null ? lower : this.minValue(); - upper = upper !== null ? upper : this.maxValue(); + // If lower or upper are null or unset, set them to minValue or maxValue + lower = lower ?? this.minValue(); + upper = upper ?? this.maxValue(); // Ensure lower does not go below minValue - lower = Math.max(lower, this.minValue()); + lower = Math.max(lower ?? 0, this.minValue() ?? 0); // Ensure upper does not exceed maxValue - if (upper > this.maxValue()) { + if ((upper ?? Infinity) > (this.maxValue() ?? Infinity)) { upper = this.maxValue(); sections.push(steelseries.Section(lower, upper, color)); break; @@ -190,11 +193,13 @@ export class GaugeSteelComponent implements OnInit, OnChanges, OnDestroy { } //Colors - if (SteelBackgroundColors[this.backgroundColor()]) { - this.gaugeOptions['backgroundColor'] = SteelBackgroundColors[this.backgroundColor()]; + const backgroundColor = this.backgroundColor(); + if (backgroundColor && SteelBackgroundColors[backgroundColor]) { + this.gaugeOptions['backgroundColor'] = SteelBackgroundColors[backgroundColor]; } - if (SteelFrameColors[this.frameColor()]) { - this.gaugeOptions['frameDesign'] = SteelFrameColors[this.frameColor()]; + const frameColor = this.frameColor(); + if (frameColor && SteelFrameColors[frameColor]) { + this.gaugeOptions['frameDesign'] = SteelFrameColors[frameColor]; } if (this.barGauge()) { this.gaugeOptions['valueColor'] = steelseries.ColorDef.GREEN; @@ -207,7 +212,7 @@ export class GaugeSteelComponent implements OnInit, OnChanges, OnDestroy { this.gaugeOptions['ledVisible'] = false; } - private setGaugeType(radialSize: string): string { + private setGaugeType(radialSize: string | undefined): string { switch (radialSize) { case 'quarter': return steelseries.GaugeType.TYPE1; diff --git a/src/app/widgets/minichart/minichart.component.ts b/src/app/widgets/minichart/minichart.component.ts index adbde194..7aca8fc5 100644 --- a/src/app/widgets/minichart/minichart.component.ts +++ b/src/app/widgets/minichart/minichart.component.ts @@ -49,7 +49,7 @@ export class MinichartComponent implements OnDestroy { public yScaleMin: number = null; public yScaleMax: number = null; public inverseYAxis = false; - public verticalChart = null; + public verticalChart: boolean | null = null; protected unitsService = inject(UnitsService); private readonly historyStream = inject(HistoryChartStreamService); private readonly ngZone = inject(NgZone); diff --git a/src/app/widgets/svg-boolean-button/svg-boolean-button.component.ts b/src/app/widgets/svg-boolean-button/svg-boolean-button.component.ts index 7cede8ee..badf13d6 100644 --- a/src/app/widgets/svg-boolean-button/svg-boolean-button.component.ts +++ b/src/app/widgets/svg-boolean-button/svg-boolean-button.component.ts @@ -12,14 +12,14 @@ import { IDimensions } from '../widget-boolean-switch/widget-boolean-switch.comp }) export class SvgBooleanButtonComponent implements DoCheck, OnDestroy { // eslint-disable-next-line @angular-eslint/no-input-rename - readonly data = input(null, { alias: "controlData" }); - readonly theme = input(null); + readonly data = input(null, { alias: "controlData" }); + readonly theme = input(null); readonly dimensions = input.required(); readonly toggleClick = output(); private toggleOff = "0 35 180 35"; private toggleOn = "0 0 180 35"; - private oldTheme: ITheme = null; + private oldTheme: ITheme | null = null; private holdTimeoutId: ReturnType | null = null; // delay before starting momentary emit private emitIntervalId: ReturnType | null = null; // repeating emit while pressed @@ -29,14 +29,15 @@ export class SvgBooleanButtonComponent implements DoCheck, OnDestroy { private pointerStartY = 0; public viewBox: string = this.toggleOff; - public labelColorEnabled = null; - public labelColorDisabled = null; - public valueColor = null; + public labelColorEnabled: string | null = null; + public labelColorDisabled: string | null = null; + public valueColor: string | null = null; private ctrlColor = ''; ngDoCheck(): void { this.viewBox = this.pressed ? this.toggleOn : this.toggleOff; const data = this.data(); + if (!data) return; if (data.color != this.ctrlColor) { this.ctrlColor = data.color; this.getColors(data.color); @@ -45,7 +46,7 @@ export class SvgBooleanButtonComponent implements DoCheck, OnDestroy { const theme = this.theme(); if (this.oldTheme != theme) { this.oldTheme = theme - this.getColors(this.data().color); + this.getColors(data.color); } } @@ -61,7 +62,9 @@ export class SvgBooleanButtonComponent implements DoCheck, OnDestroy { // Momentary mode this.pressed = true; - const state: IDynamicControl = cloneDeep(this.data()); + const data = this.data(); + if (!data) return; + const state: IDynamicControl = cloneDeep(data); state.value = this.pressed; // Start emitting the state every 100ms @@ -110,51 +113,53 @@ export class SvgBooleanButtonComponent implements DoCheck, OnDestroy { } private getColors(color: string): void { + const theme = this.theme(); + if (!theme) return; switch (color) { case "contrast": this.labelColorEnabled = 'black'; - this.labelColorDisabled = this.theme().contrastDim; - this.valueColor = this.theme().contrast; + this.labelColorDisabled = theme.contrastDim; + this.valueColor = theme.contrast; break; case "blue": - this.labelColorEnabled = this.theme().contrast; - this.labelColorDisabled = this.theme().blueDim; - this.valueColor = this.theme().blue; + this.labelColorEnabled = theme.contrast; + this.labelColorDisabled = theme.blueDim; + this.valueColor = theme.blue; break; case "green": - this.labelColorEnabled = this.theme().contrast; - this.labelColorDisabled = this.theme().greenDim; - this.valueColor = this.theme().green; + this.labelColorEnabled = theme.contrast; + this.labelColorDisabled = theme.greenDim; + this.valueColor = theme.green; break; case "pink": - this.labelColorEnabled = this.theme().contrast; - this.labelColorDisabled = this.theme().pinkDim; - this.valueColor = this.theme().pink; + this.labelColorEnabled = theme.contrast; + this.labelColorDisabled = theme.pinkDim; + this.valueColor = theme.pink; break; case "orange": - this.labelColorEnabled = this.theme().contrast; - this.labelColorDisabled = this.theme().orangeDim; - this.valueColor = this.theme().orange; + this.labelColorEnabled = theme.contrast; + this.labelColorDisabled = theme.orangeDim; + this.valueColor = theme.orange; break; case "purple": - this.labelColorEnabled = this.theme().contrast; - this.labelColorDisabled = this.theme().purpleDim; - this.valueColor = this.theme().purple; + this.labelColorEnabled = theme.contrast; + this.labelColorDisabled = theme.purpleDim; + this.valueColor = theme.purple; break; case "grey": - this.labelColorEnabled = this.theme().contrast; - this.labelColorDisabled = this.theme().greyDim; - this.valueColor = this.theme().grey; + this.labelColorEnabled = theme.contrast; + this.labelColorDisabled = theme.greyDim; + this.valueColor = theme.grey; break; case "yellow": - this.labelColorEnabled = this.theme().contrast; - this.labelColorDisabled = this.theme().yellowDim; - this.valueColor = this.theme().yellow; + this.labelColorEnabled = theme.contrast; + this.labelColorDisabled = theme.yellowDim; + this.valueColor = theme.yellow; break; default: this.labelColorEnabled = 'black'; - this.labelColorDisabled = this.theme().contrastDim; - this.valueColor = this.theme().contrast; + this.labelColorDisabled = theme.contrastDim; + this.valueColor = theme.contrast; break; } } diff --git a/src/app/widgets/svg-boolean-light/svg-boolean-light.component.ts b/src/app/widgets/svg-boolean-light/svg-boolean-light.component.ts index 42c91d78..95059ce1 100644 --- a/src/app/widgets/svg-boolean-light/svg-boolean-light.component.ts +++ b/src/app/widgets/svg-boolean-light/svg-boolean-light.component.ts @@ -13,25 +13,26 @@ import { IDimensions } from '../widget-boolean-switch/widget-boolean-switch.comp }) export class SvgBooleanLightComponent implements DoCheck { // eslint-disable-next-line @angular-eslint/no-input-rename - readonly data = input(null, { alias: "controlData" }); - readonly theme = input(null); + readonly data = input(null, { alias: "controlData" }); + readonly theme = input(null); readonly dimensions = input.required(); readonly toggleClick = output(); private toggleOff = "0 35 180 35"; private toggleOn = "0 0 180 35"; - private ctrlState: boolean = null; + private ctrlState: boolean | null = null; private ctrlColor = ''; - private oldTheme: ITheme = null; + private oldTheme: ITheme | null = null; public viewBox: string = this.toggleOff; - public labelColor = null; - public valueColor = null; + public labelColor: string | null = null; + public valueColor: string | null = null; constructor() { } ngDoCheck(): void { const data = this.data(); + if (!data) return; if (data.value != this.ctrlState) { this.ctrlState = data.value; this.viewBox = data.value ? this.toggleOn : this.toggleOff; @@ -50,47 +51,50 @@ export class SvgBooleanLightComponent implements DoCheck { public toggle(state: boolean): void { const data = this.data(); + if (!data) return; data.value = state; this.toggleClick.emit(data); } private getColors(color: string): void { + const theme = this.theme(); + if (!theme) return; switch (color) { case "contrast": - this.labelColor = this.theme().contrastDim; - this.valueColor = this.theme().contrast; + this.labelColor = theme.contrastDim; + this.valueColor = theme.contrast; break; case "blue": - this.labelColor = this.theme().blueDim; - this.valueColor = this.theme().blue; + this.labelColor = theme.blueDim; + this.valueColor = theme.blue; break; case "green": - this.labelColor = this.theme().greenDim; - this.valueColor = this.theme().green; + this.labelColor = theme.greenDim; + this.valueColor = theme.green; break; case "pink": - this.labelColor = this.theme().pinkDim; - this.valueColor = this.theme().pink; + this.labelColor = theme.pinkDim; + this.valueColor = theme.pink; break; case "orange": - this.labelColor = this.theme().orangeDim; - this.valueColor = this.theme().orange; + this.labelColor = theme.orangeDim; + this.valueColor = theme.orange; break; case "purple": - this.labelColor = this.theme().purpleDim; - this.valueColor = this.theme().purple; + this.labelColor = theme.purpleDim; + this.valueColor = theme.purple; break; case "grey": - this.labelColor = this.theme().greyDim; - this.valueColor = this.theme().grey; + this.labelColor = theme.greyDim; + this.valueColor = theme.grey; break; case "yellow": - this.labelColor = this.theme().yellowDim; - this.valueColor = this.theme().yellow; + this.labelColor = theme.yellowDim; + this.valueColor = theme.yellow; break; default: - this.labelColor = this.theme().contrastDim; - this.valueColor = this.theme().contrast; + this.labelColor = theme.contrastDim; + this.valueColor = theme.contrast; break; } } diff --git a/src/app/widgets/svg-boolean-switch/svg-boolean-switch.component.ts b/src/app/widgets/svg-boolean-switch/svg-boolean-switch.component.ts index 195fceb8..fd8c2a06 100644 --- a/src/app/widgets/svg-boolean-switch/svg-boolean-switch.component.ts +++ b/src/app/widgets/svg-boolean-switch/svg-boolean-switch.component.ts @@ -11,26 +11,27 @@ import { createSwipeGuard } from '../../core/utils/pointer-swipe-guard.util'; }) export class SvgBooleanSwitchComponent implements DoCheck { // eslint-disable-next-line @angular-eslint/no-input-rename - readonly data = input(null, { alias: "controlData" }); - readonly theme = input(null); + readonly data = input(null, { alias: "controlData" }); + readonly theme = input(null); readonly dimensions = input.required(); readonly toggleClick = output(); private toggleOff = "0 35 180 35"; private toggleOn = "0 0 180 35"; - private ctrlState: boolean = null; - private oldTheme: ITheme = null; + private ctrlState: boolean | null = null; + private oldTheme: ITheme | null = null; private readonly swipeGuard = createSwipeGuard(); public viewBox: string = this.toggleOff; - public labelColor = null; - public valueColor = null; + public labelColor: string | null = null; + public valueColor: string | null = null; private ctrlColor = ''; constructor() { } ngDoCheck(): void { const data = this.data(); + if (!data) return; if (data.value != this.ctrlState) { this.ctrlState = data.value; this.viewBox = data.value ? this.toggleOn : this.toggleOff; @@ -66,47 +67,50 @@ export class SvgBooleanSwitchComponent implements DoCheck { public toggle(state: boolean): void { const data = this.data(); + if (!data) return; data.value = state; this.toggleClick.emit(data); } private getColors(color: string): void { + const theme = this.theme(); + if (!theme) return; switch (color) { case "contrast": - this.labelColor = this.theme().contrastDim; - this.valueColor = this.theme().contrast; + this.labelColor = theme.contrastDim; + this.valueColor = theme.contrast; break; case "blue": - this.labelColor = this.theme().blueDim; - this.valueColor = this.theme().blue; + this.labelColor = theme.blueDim; + this.valueColor = theme.blue; break; case "green": - this.labelColor = this.theme().greenDim; - this.valueColor = this.theme().green; + this.labelColor = theme.greenDim; + this.valueColor = theme.green; break; case "pink": - this.labelColor = this.theme().pinkDim; - this.valueColor = this.theme().pink; + this.labelColor = theme.pinkDim; + this.valueColor = theme.pink; break; case "orange": - this.labelColor = this.theme().orangeDim; - this.valueColor = this.theme().orange; + this.labelColor = theme.orangeDim; + this.valueColor = theme.orange; break; case "purple": - this.labelColor = this.theme().purpleDim; - this.valueColor = this.theme().purple; + this.labelColor = theme.purpleDim; + this.valueColor = theme.purple; break; case "grey": - this.labelColor = this.theme().greyDim; - this.valueColor = this.theme().grey; + this.labelColor = theme.greyDim; + this.valueColor = theme.grey; break; case "yellow": - this.labelColor = this.theme().yellowDim; - this.valueColor = this.theme().yellow; + this.labelColor = theme.yellowDim; + this.valueColor = theme.yellow; break; default: - this.labelColor = this.theme().contrastDim; - this.valueColor = this.theme().contrast; + this.labelColor = theme.contrastDim; + this.valueColor = theme.contrast; break; } } diff --git a/src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.ts b/src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.ts index bfc58ad1..1e834825 100644 --- a/src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.ts +++ b/src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.ts @@ -94,10 +94,12 @@ export class WidgetGaugeNgLinearComponent implements AfterViewInit { private adjustedScale = computed(() => { const cfg = this.runtime.options(); if (!cfg) return { min: 0, max: 100, majorTicks: [] }; + const lower = cfg.displayScale?.lower ?? 0; + const upper = cfg.displayScale?.upper ?? 100; if (cfg.gauge?.enableTicks) { - return adjustLinearScaleAndMajorTicks(cfg.displayScale.lower, cfg.displayScale.upper); + return adjustLinearScaleAndMajorTicks(lower, upper); } - return { min: cfg.displayScale.lower, max: cfg.displayScale.upper, majorTicks: [] }; + return { min: lower, max: upper, majorTicks: [] }; }); private highlights = computed(() => { const zones = this.metadata.zones(); @@ -123,11 +125,13 @@ export class WidgetGaugeNgLinearComponent implements AfterViewInit { untracked(() => this.streams.observe('gaugePath', path => { const raw = (path?.data?.value as number) ?? null; + const lower = cfg.displayScale?.lower ?? 0; + const upper = cfg.displayScale?.upper ?? 100; if (raw == null) { - this.value.set(cfg.displayScale.lower); + this.value.set(lower); this.textValue.set('--'); } else { - const clamped = Math.min(Math.max(raw, cfg.displayScale.lower), cfg.displayScale.upper); + const clamped = Math.min(Math.max(raw, lower), upper); this.value.set(clamped); if (this.textValue() === '--') this.textValue.set(''); } @@ -155,7 +159,7 @@ export class WidgetGaugeNgLinearComponent implements AfterViewInit { this.ngGauge()?.update({ highlights: [] }); } else { const serialized = JSON.stringify(hl) as unknown as string; // gauge lib tolerates stringified form - this.ngGauge()?.update({ highlights: serialized, highlightsWidth: this.runtime.options().gauge?.highlightsWidth }); + this.ngGauge()?.update({ highlights: serialized, highlightsWidth: this.runtime.options()?.gauge?.highlightsWidth }); } } catch { /* ignore */ } }); @@ -168,7 +172,9 @@ export class WidgetGaugeNgLinearComponent implements AfterViewInit { const scale = this.adjustedScale(); untracked(() => { - this.buildGaugeOptions(this.runtime.options(), theme, scale); + const cfg = this.runtime.options(); + if (!cfg || !theme) return; + this.buildGaugeOptions(cfg, theme, scale); if (this.viewReady()) { try { this.ngGauge()?.update(this.gaugeOptions); @@ -199,11 +205,11 @@ export class WidgetGaugeNgLinearComponent implements AfterViewInit { untracked(() => { const cfg = this.runtime.options(); const theme = this.theme(); - if (cfg.ignoreZones) return; + if (!cfg || !theme || cfg.ignoreZones) return; const opt: LinearGaugeOptions = {}; const enableNeedle = cfg.gauge?.enableNeedle; - const palette = getColors(cfg.color, theme); + const palette = getColors(cfg.color ?? 'contrast', theme); switch (state) { case States.Alarm: if (enableNeedle) { @@ -275,7 +281,7 @@ export class WidgetGaugeNgLinearComponent implements AfterViewInit { opt.fontNumbers = 'Roboto'; opt.fontNumbersWeight = 'normal'; opt.fontUnitsSize = isVertical ? 40 : 35; opt.colorTitle = getColors('contrast', theme).dim; opt.colorUnits = getColors('contrast', theme).dim; opt.colorValueBoxBackground = theme.background; - const palette = getColors(cfg.color, theme); + const palette = getColors(cfg.color ?? 'contrast', theme); // baseline colors opt.colorValueText = palette.color; if (enableNeedle) { diff --git a/src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.ts b/src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.ts index 4ede7b99..84b04623 100644 --- a/src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.ts +++ b/src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.ts @@ -174,7 +174,7 @@ export class WidgetGaugeNgRadialComponent implements AfterViewInit { this.ngGauge()?.update({ highlights: [] }); } else { const serialized = JSON.stringify(hl) as unknown as string; // gauge lib tolerates stringified form - this.ngGauge()?.update({ highlights: serialized, highlightsWidth: this.runtime.options().gauge?.highlightsWidth }); + this.ngGauge()?.update({ highlights: serialized, highlightsWidth: this.runtime.options()?.gauge?.highlightsWidth }); } } catch { /* ignore */ } }); @@ -187,7 +187,9 @@ export class WidgetGaugeNgRadialComponent implements AfterViewInit { const scale = this.adjustedScale(); // reading for dependency untracked(() => { - this.buildGaugeOptions(this.runtime.options(), theme, scale); + const cfg = this.runtime.options(); + if (!cfg || !theme) return; + this.buildGaugeOptions(cfg, theme, scale); if (this.viewReady()) { try { this.ngGauge()?.update(this.gaugeOptions); @@ -217,7 +219,7 @@ export class WidgetGaugeNgRadialComponent implements AfterViewInit { untracked(() => { const cfg = this.runtime.options(); const theme = this.theme(); - if (cfg.ignoreZones) return; + if (!cfg || !theme || cfg.ignoreZones) return; const option: RadialGaugeOptions = {}; switch (state) { @@ -238,8 +240,8 @@ export class WidgetGaugeNgRadialComponent implements AfterViewInit { break; default: option.colorBorderMiddle = theme.cardColor; - option.colorBarProgress = cfg.gauge?.subType === 'measuring' ? getColors(cfg.color, theme).color : getColors(cfg.color, theme).dim; - option.colorValueText = getColors(cfg.color, theme).color; + option.colorBarProgress = cfg.gauge?.subType === 'measuring' ? getColors(cfg.color ?? 'contrast', theme).color : getColors(cfg.color ?? 'contrast', theme).dim; + option.colorValueText = getColors(cfg.color ?? 'contrast', theme).color; } try { this.ngGauge()?.update(option); @@ -306,12 +308,12 @@ export class WidgetGaugeNgRadialComponent implements AfterViewInit { g.colorBorderShadow = false; g.colorBorderOuter = theme.cardColor; g.colorBorderOuterEnd = ''; g.colorBorderMiddle = theme.cardColor; g.colorBorderMiddleEnd = ''; g.colorPlate = g.colorPlateEnd = theme.cardColor; g.colorBar = theme.background; - g.barProgress = cfg.gauge?.enableProgressbar; g.colorBarProgress = getColors(cfg.color, theme).color; - g.colorNeedle = getColors(cfg.color, theme).color; g.colorNeedleEnd = getColors(cfg.color, theme).color; + g.barProgress = cfg.gauge?.enableProgressbar; g.colorBarProgress = getColors(cfg.color ?? 'contrast', theme).color; + g.colorNeedle = getColors(cfg.color ?? 'contrast', theme).color; g.colorNeedleEnd = getColors(cfg.color ?? 'contrast', theme).color; g.needleShadow = true; g.colorNeedleShadowUp = "black"; g.colorNeedleShadowDown = "black"; g.colorNeedleCircleInner = g.colorPlate; g.colorNeedleCircleInnerEnd = g.colorPlate; g.colorNeedleCircleOuter = g.colorPlate; g.colorNeedleCircleOuterEnd = g.colorPlate; - g.colorTitle = theme.contrastDim; g.colorUnits = theme.contrastDim; g.colorValueText = getColors(cfg.color, theme).color; + g.colorTitle = theme.contrastDim; g.colorUnits = theme.contrastDim; g.colorValueText = getColors(cfg.color ?? 'contrast', theme).color; this.colorStrokeTicks.set(theme.contrastDim); g.colorMinorTicks = theme.contrastDim; g.animationTarget = this.ANIMATION_TARGET_NEEDLE; g.useMinPath = false; @@ -321,7 +323,7 @@ export class WidgetGaugeNgRadialComponent implements AfterViewInit { g.valueBox = true; g.fontValueSize = 60; g.valueBoxWidth = 10; g.valueBoxBorderRadius = 5; g.valueBoxStroke = 0; g.colorValueBoxBackground = ''; g.colorMajorTicks = g.colorPlate; g.colorNumbers = g.colorMinorTicks = '' as unknown as string; - g.barWidth = 20; g.colorBarProgress = getColors(cfg.color, theme).dim; + g.barWidth = 20; g.colorBarProgress = getColors(cfg.color ?? 'contrast', theme).dim; g.needle = cfg.gauge.enableNeedle; g.needleType = this.LINE; g.needleWidth = 2; g.needleStart = 75; g.needleEnd = 95; g.needleCircleSize = 1; g.needleCircleInner = false; g.needleCircleOuter = false; g.ticksAngle = 360; g.startAngle = (cfg.gauge?.scaleStart as number) || 180; g.majorTicks = 0 as unknown as string[]; g.exactTicks = true; g.strokeTicks = false; g.minorTicks = 0; g.numbersMargin = 0; g.fontNumbersSize = 0; g.borders = true; g.borderOuterWidth = 2; g.borderMiddleWidth = 1; g.borderInnerWidth = 0; g.borderShadowWidth = 0; @@ -329,9 +331,9 @@ export class WidgetGaugeNgRadialComponent implements AfterViewInit { } else { // measuring g.fontTitleSize = 24; g.barWidth = 15; g.valueBox = true; g.fontValueSize = 60; g.valueBoxWidth = 100; g.valueBoxBorderRadius = 0; g.valueBoxStroke = 0; g.colorValueBoxBackground = ''; - g.needle = cfg.gauge.enableNeedle; g.needleType = this.LINE; g.needleWidth = 2; g.needleStart = 0; g.needleEnd = 95; g.needleCircleSize = 10; g.needleCircleInner = false; g.needleCircleOuter = false; + g.needle = cfg.gauge?.enableNeedle; g.needleType = this.LINE; g.needleWidth = 2; g.needleStart = 0; g.needleEnd = 95; g.needleCircleSize = 10; g.needleCircleInner = false; g.needleCircleOuter = false; g.ticksAngle = 270; g.startAngle = 45; g.barStartPosition = cfg.gauge?.barStartPosition || 'left'; - if (cfg.gauge.enableTicks) { + if (cfg.gauge?.enableTicks) { g.strokeTicks = true; g.majorTicks = scale.majorTicks as unknown as string[]; g.minorTicks = 2; g.exactTicks = false; g.numbersMargin = 3; g.fontNumbersSize = 15; g.colorMajorTicks = theme.contrastDim; g.colorNumbers = theme.contrastDim; } else { diff --git a/src/app/widgets/widget-numeric/numeric-minmax.util.spec.ts b/src/app/widgets/widget-numeric/numeric-minmax.util.spec.ts new file mode 100644 index 00000000..98f326fe --- /dev/null +++ b/src/app/widgets/widget-numeric/numeric-minmax.util.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { reduceMinMax } from './numeric-minmax.util'; + +describe('reduceMinMax', () => { + it('seeds min from the first sample (max is set by a later non-min sample)', () => { + expect(reduceMinMax(null, null, 5)).toEqual({ min: 5, max: null }); + }); + + it('lowers min on a new low', () => { + expect(reduceMinMax(5, 10, 3)).toEqual({ min: 3, max: 10 }); + }); + + it('raises max on a new high', () => { + expect(reduceMinMax(3, 10, 12)).toEqual({ min: 3, max: 12 }); + }); + + it('leaves min/max unchanged for an in-range sample', () => { + expect(reduceMinMax(3, 10, 7)).toEqual({ min: 3, max: 10 }); + }); + + it('preserves the tracked min/max on a null sample (sensor dropout)', () => { + // Regression guard: previously a null coerced to 0 in the comparison and wiped + // the running min (positive paths) or max (negative paths). + expect(reduceMinMax(3, 10, null)).toEqual({ min: 3, max: 10 }); + expect(reduceMinMax(-8, -2, null)).toEqual({ min: -8, max: -2 }); + }); +}); diff --git a/src/app/widgets/widget-numeric/numeric-minmax.util.ts b/src/app/widgets/widget-numeric/numeric-minmax.util.ts new file mode 100644 index 00000000..e6e040af --- /dev/null +++ b/src/app/widgets/widget-numeric/numeric-minmax.util.ts @@ -0,0 +1,23 @@ +/** + * Folds a new sample into the running min/max the numeric widget displays. + * + * A `null` sample (Signal K emits null on timeout/sensor loss) leaves the tracked + * extremes untouched — it must not reset them. The min/max update is mutually + * exclusive per call, matching the widget's long-standing behaviour. + */ +export function reduceMinMax( + min: number | null, + max: number | null, + value: number | null +): { min: number | null; max: number | null } { + if (value === null) { + return { min, max }; + } + if (min === null || value < min) { + return { min: value, max }; + } + if (max === null || value > max) { + return { min, max: value }; + } + return { min, max }; +} diff --git a/src/app/widgets/widget-numeric/widget-numeric.component.ts b/src/app/widgets/widget-numeric/widget-numeric.component.ts index 09393f79..12972df7 100644 --- a/src/app/widgets/widget-numeric/widget-numeric.component.ts +++ b/src/app/widgets/widget-numeric/widget-numeric.component.ts @@ -2,6 +2,7 @@ import { Component, OnDestroy, AfterViewInit, ElementRef, inject, signal, viewCh import { ChangeDetectionStrategy } from '@angular/core'; import { IWidgetSvcConfig } from '../../core/interfaces/widgets-interface'; import { MinichartComponent } from '../minichart/minichart.component'; +import { reduceMinMax } from './numeric-minmax.util'; import { WidgetRuntimeDirective } from '../../core/directives/widget-runtime.directive'; import { WidgetStreamsDirective } from '../../core/directives/widget-streams.directive'; import { IPathUpdate } from '../../core/services/data.service'; @@ -60,19 +61,19 @@ export class WidgetNumericComponent implements OnInit, AfterViewInit, OnDestroy private canvasMainRef = viewChild.required>('canvasMainRef'); protected showMiniChart = signal(false); - protected labelColor = signal(undefined); + protected labelColor = signal(''); private canvasElement: HTMLCanvasElement; - private canvasCtx: CanvasRenderingContext2D; + private canvasCtx: CanvasRenderingContext2D | null; private cssWidth = 0; private cssHeight = 0; private backgroundBitmap: HTMLCanvasElement | null = null; private backgroundBitmapText: string | null = null; - private dataValue: number = null; - private maxValue: number = null; - private minValue: number = null; - private valueColor: string = undefined; - private valueStateColor: string = undefined; + private dataValue: number | null = null; + private maxValue: number | null = null; + private minValue: number | null = null; + private valueColor: string | undefined = undefined; + private valueStateColor: string | undefined = undefined; private maxValueTextWidth = 0; private maxValueTextHeight = 0; private maxMinMaxTextWidth = 0; @@ -98,25 +99,25 @@ export class WidgetNumericComponent implements OnInit, AfterViewInit, OnDestroy }); private onNumericValue = (newValue: IPathUpdate) => { - this.dataValue = newValue.data.value as number; - if (this.minValue === null || this.dataValue < this.minValue) { - this.minValue = this.dataValue; - } else if (this.maxValue === null || this.dataValue > this.maxValue) { - this.maxValue = this.dataValue; - } + const dataValue = newValue.data.value as number | null; + this.dataValue = dataValue; + const minMax = reduceMinMax(this.minValue, this.maxValue, dataValue); + this.minValue = minMax.min; + this.maxValue = minMax.max; - if (!this.runtime?.options().ignoreZones) { + if (!this.runtime?.options()?.ignoreZones) { if (this.pathDataState !== newValue.state) { this.pathDataState = newValue.state as States; + const theme = this.theme(); switch (newValue.state) { case States.Alarm: - this.valueStateColor = this.theme().zoneAlarm; + this.valueStateColor = theme?.zoneAlarm ?? this.valueColor; break; case States.Warn: - this.valueStateColor = this.theme().zoneWarn; + this.valueStateColor = theme?.zoneWarn ?? this.valueColor; break; case States.Alert: - this.valueStateColor = this.theme().zoneAlert; + this.valueStateColor = theme?.zoneAlert ?? this.valueColor; break; default: this.valueStateColor = this.valueColor; @@ -128,7 +129,7 @@ export class WidgetNumericComponent implements OnInit, AfterViewInit, OnDestroy }; constructor() { - this.showMiniChart.set(this.runtime.options().showMiniChart); + this.showMiniChart.set(this.runtime.options()?.showMiniChart ?? false); effect(() => { const theme = this.theme(); const color = this.runtime?.options()?.color; @@ -186,8 +187,8 @@ export class WidgetNumericComponent implements OnInit, AfterViewInit, OnDestroy if (!miniChartSignature) return; if (!show) return; if (!chart) return; // will re-run when present - this.setMiniChart(); - this.miniChart().startChart(); + this.setMiniChart(chart); + chart.startChart(); }); } @@ -226,62 +227,66 @@ export class WidgetNumericComponent implements OnInit, AfterViewInit, OnDestroy } private updateMiniChartVisibility(): void { - this.showMiniChart.set(!!this.runtime.options().showMiniChart); + this.showMiniChart.set(!!this.runtime.options()?.showMiniChart); } - private setMiniChart(): void { + private setMiniChart(chart: MinichartComponent): void { const cfg = this.runtime.options(); - const pathInfo = cfg.paths['numericPath']; - this.miniChart().dataPath = pathInfo?.path ?? null; - this.miniChart().dataSource = pathInfo?.source ?? 'default'; - this.miniChart().color = cfg.color; - this.miniChart().convertUnitTo = pathInfo?.convertUnitTo; - this.miniChart().numDecimal = cfg.numDecimal; - this.miniChart().yScaleMin = cfg.yScaleMin; - this.miniChart().yScaleMax = cfg.yScaleMax; - this.miniChart().inverseYAxis = cfg.inverseYAxis; - this.miniChart().verticalChart = cfg.verticalChart; + if (!cfg) return; + const pathInfo = cfg.paths?.['numericPath']; + chart.dataPath = pathInfo?.path ?? null; + chart.dataSource = pathInfo?.source ?? 'default'; + chart.color = cfg.color ?? 'contrast'; + chart.convertUnitTo = pathInfo?.convertUnitTo; + chart.numDecimal = cfg.numDecimal ?? 1; + chart.yScaleMin = cfg.yScaleMin ?? 0; + chart.yScaleMax = cfg.yScaleMax ?? 10; + chart.inverseYAxis = cfg.inverseYAxis ?? false; + chart.verticalChart = cfg.verticalChart ?? false; } private setColors(): void { const cfg = this.runtime.options(); - if (!cfg) return; - this.labelColor.set(getColors(cfg.color, this.theme()).dim); - this.valueStateColor = this.valueColor = getColors(cfg.color, this.theme()).color; + const theme = this.theme(); + if (!cfg || !theme) return; + this.labelColor.set(getColors(cfg.color ?? 'contrast', theme).dim); + this.valueStateColor = this.valueColor = getColors(cfg.color ?? 'contrast', theme).color; this.backgroundBitmap = null; this.backgroundBitmapText = null; } private drawWidget(): void { - if (!this.canvasCtx) return; + const ctx = this.canvasCtx; + if (!ctx) return; const cfg = this.runtime.options(); if (!cfg) return; - const unit = cfg.paths['numericPath'].convertUnitTo; + const unit = cfg.paths?.['numericPath']?.convertUnitTo; const marginX = 10 * this.canvas.scaleFactor; const marginY = 5 * this.canvas.scaleFactor; - const bgText = cfg.displayName + '|' + unit; + const displayName = cfg.displayName ?? 'Gauge Label'; + const bgText = displayName + '|' + (unit ?? ''); if (!this.backgroundBitmap || this.backgroundBitmap.width !== this.canvasElement.width || this.backgroundBitmap.height !== this.canvasElement.height || this.backgroundBitmapText !== bgText) { this.backgroundBitmap = this.canvas.renderStaticToBitmap( - this.canvasCtx, + ctx, this.cssWidth, this.cssHeight, - (ctx) => { + (bitmapCtx) => { this.canvas['drawTitleInternal']( - ctx, - cfg.displayName, + bitmapCtx, + displayName, this.labelColor(), 'normal', this.cssWidth, this.cssHeight, 0.1 ); - if (!['unitless', 'percent', 'ratio', 'latitudeSec', 'latitudeMin', 'longitudeSec', 'longitudeMin'].includes(unit)) { + if (unit !== undefined && !['unitless', 'percent', 'ratio', 'latitudeSec', 'latitudeMin', 'longitudeSec', 'longitudeMin'].includes(unit)) { this.canvas.drawText( - ctx, + bitmapCtx, unit, this.cssWidth - marginX, this.cssHeight - marginY, @@ -298,21 +303,21 @@ export class WidgetNumericComponent implements OnInit, AfterViewInit, OnDestroy this.backgroundBitmapText = bgText; } - this.canvas.clearCanvas(this.canvasCtx, this.cssWidth, this.cssHeight); + this.canvas.clearCanvas(ctx, this.cssWidth, this.cssHeight); if (this.backgroundBitmap && this.backgroundBitmap.width > 0 && this.backgroundBitmap.height > 0) { - this.canvasCtx.drawImage(this.backgroundBitmap, 0, 0, this.cssWidth, this.cssHeight); + ctx.drawImage(this.backgroundBitmap, 0, 0, this.cssWidth, this.cssHeight); } - this.drawValue(); + this.drawValue(ctx); if (cfg.showMax || cfg.showMin) { - this.drawMinMax(); + this.drawMinMax(ctx); } } - private drawValue(): void { + private drawValue(ctx: CanvasRenderingContext2D): void { const valueText = this.getValueText(); this.canvas.drawText( - this.canvasCtx, + ctx, valueText, Math.floor(this.cssWidth / 2), Math.floor((this.cssHeight / 2) * 1.15), @@ -324,16 +329,19 @@ export class WidgetNumericComponent implements OnInit, AfterViewInit, OnDestroy } private getValueText(): string { - if (this.dataValue === null) return "--"; - const cUnit = this.runtime.options().paths['numericPath'].convertUnitTo; - if (['latitudeSec', 'latitudeMin', 'longitudeSec', 'longitudeMin', 'D HH:MM:SS'].includes(cUnit)) { - return this.dataValue.toString(); + const dataValue = this.dataValue; + if (dataValue === null) return "--"; + const cfg = this.runtime.options(); + const cUnit = cfg?.paths?.['numericPath']?.convertUnitTo; + if (cUnit !== undefined && ['latitudeSec', 'latitudeMin', 'longitudeSec', 'longitudeMin', 'D HH:MM:SS'].includes(cUnit)) { + return dataValue.toString(); } - return this.applyDecorations(this.dataValue.toFixed(this.runtime.options().numDecimal)); + return this.applyDecorations(dataValue.toFixed(cfg?.numDecimal)); } - private drawMinMax(): void { + private drawMinMax(ctx: CanvasRenderingContext2D): void { const cfg = this.runtime.options(); + if (!cfg) return; if (!cfg.showMin && !cfg.showMax) return; let valueText = ''; if (cfg.showMin) { @@ -346,7 +354,7 @@ export class WidgetNumericComponent implements OnInit, AfterViewInit, OnDestroy const marginX = 10 * this.canvas.scaleFactor; const marginY = 5 * this.canvas.scaleFactor; this.canvas.drawText( - this.canvasCtx, + ctx, valueText, marginX, Math.floor(this.cssHeight - marginY), @@ -360,7 +368,7 @@ export class WidgetNumericComponent implements OnInit, AfterViewInit, OnDestroy } private applyDecorations(txtValue: string): string { - switch (this.runtime.options().paths['numericPath'].convertUnitTo) { + switch (this.runtime.options()?.paths?.['numericPath']?.convertUnitTo) { case 'percent': case 'percentraw': txtValue += '%'; diff --git a/src/app/widgets/widget-simple-linear/widget-simple-linear.component.ts b/src/app/widgets/widget-simple-linear/widget-simple-linear.component.ts index afd775a6..989c7144 100644 --- a/src/app/widgets/widget-simple-linear/widget-simple-linear.component.ts +++ b/src/app/widgets/widget-simple-linear/widget-simple-linear.component.ts @@ -58,7 +58,7 @@ export class WidgetSimpleLinearComponent { // Signals (presentation state) protected readonly unitsLabel = signal(''); protected readonly dataLabelValue = signal('0'); - protected readonly dataValue = signal(null); + protected readonly dataValue = signal(null); protected readonly barColor = signal(''); protected readonly barColorGradient = signal(''); protected readonly barColorBackground = signal(''); @@ -72,9 +72,9 @@ export class WidgetSimpleLinearComponent { const zones = this.metadata.zones(); if (!zones?.length) return []; - const unit = cfg.paths['gaugePath'].convertUnitTo; - const min = cfg.displayScale.lower; - const max = cfg.displayScale.upper; + const unit = cfg.paths?.['gaugePath'].convertUnitTo; + const min = cfg.displayScale?.lower ?? 0; + const max = cfg.displayScale?.upper ?? 15; return getHighlights(zones, theme, unit, this.unitsService, min, max); }); private lastState: States | null = null; // simple cache to avoid redundant color sets @@ -92,10 +92,12 @@ export class WidgetSimpleLinearComponent { const raw = pkt?.data?.value as number | null; // Clamp & label formatting if (raw == null) { - this.dataValue.set(cfg.displayScale.lower); + this.dataValue.set(cfg.displayScale?.lower ?? 0); this.dataLabelValue.set('--'); } else { - const clamped = Math.min(Math.max(raw, cfg.displayScale.lower), cfg.displayScale.upper); + const lower = cfg.displayScale?.lower ?? 0; + const upper = cfg.displayScale?.upper ?? 15; + const clamped = Math.min(Math.max(raw, lower), upper); this.dataValue.set(clamped); this.dataLabelValue.set(clamped.toFixed(cfg.numDecimal)); } @@ -109,15 +111,15 @@ export class WidgetSimpleLinearComponent { case States.Warn: this.barColor.set(theme.zoneWarn); break; case States.Alert: this.barColor.set(theme.zoneAlert); break; case States.Nominal: this.barColor.set(theme.zoneNominal); break; - default: this.barColor.set(getColors(cfg.color, theme).color); break; + default: this.barColor.set(getColors(cfg.color ?? 'contrast', theme).color); break; } } } }); // Unit label (abr|full) - const unit = cfg.paths['gaugePath'].convertUnitTo; - this.unitsLabel.set(cfg.gauge.unitLabelFormat === 'abr' ? unit?.substring(0,1) : unit); + const unit = cfg.paths?.['gaugePath'].convertUnitTo; + this.unitsLabel.set(cfg.gauge?.unitLabelFormat === 'abr' ? unit?.substring(0,1) : unit); }); }); @@ -128,7 +130,7 @@ export class WidgetSimpleLinearComponent { if (!cfg || !theme) return; untracked(() => { this.barColorBackground.set(theme.background); - const palette = getColors(cfg.color, theme); + const palette = getColors(cfg.color ?? 'contrast', theme); // Set baseline colors (and recompute any zone-derived color on theme changes) if (cfg.ignoreZones) { this.barColor.set(palette.color); @@ -152,8 +154,9 @@ export class WidgetSimpleLinearComponent { // Zones metadata observation (only when needed) effect(() => { const cfg = this.runtime.options(); - if (!cfg || cfg.ignoreZones || !this.metadata) return; - untracked(() => this.metadata.observe('gaugePath')); + const metadata = this.metadata; + if (!cfg || cfg.ignoreZones || !metadata) return; + untracked(() => metadata.observe('gaugePath')); }); } } diff --git a/src/app/widgets/widget-slider/widget-slider.component.ts b/src/app/widgets/widget-slider/widget-slider.component.ts index 9548a458..a7fc0d0d 100644 --- a/src/app/widgets/widget-slider/widget-slider.component.ts +++ b/src/app/widgets/widget-slider/widget-slider.component.ts @@ -36,7 +36,6 @@ export class WidgetSliderComponent implements OnInit, OnDestroy { showPathSkUnitsFilter: false, pathSkUnitsFilter: null, showConvertUnitTo: false, - convertUnitTo: null, supportsPut: true, sampleTime: 500 } @@ -57,8 +56,8 @@ export class WidgetSliderComponent implements OnInit, OnDestroy { private readonly signalkRequestsService = inject(SignalkRequestsService); private readonly toast = inject(ToastService); - protected labelColor = signal(undefined) - protected barColor = signal(undefined); + protected labelColor = signal('') + protected barColor = signal(''); private lineStartPx: number; private lineWidthPx: number; @@ -86,7 +85,7 @@ export class WidgetSliderComponent implements OnInit, OnDestroy { if (!cfg || !theme) return; untracked(() => { this.ensureColorMap(); - this.getColors(cfg.color); + this.getColors(cfg.color ?? 'contrast'); }); }); @@ -100,7 +99,8 @@ export class WidgetSliderComponent implements OnInit, OnDestroy { this.streams.observe('gaugePath', (newValue) => { if (!newValue || !newValue.data) { - queueMicrotask(() => this.updateHandlePosition(this.mapValueToPosition(cfg.displayScale.lower))); + const lower = this.getScaleBounds(cfg).lower; + queueMicrotask(() => this.updateHandlePosition(this.mapValueToPosition(lower))); return; } queueMicrotask(() => this.updateHandlePosition(this.mapValueToPosition(newValue.data.value as number))); @@ -113,7 +113,7 @@ export class WidgetSliderComponent implements OnInit, OnDestroy { const cfg = this.runtime.options(); if (cfg) { - this.getColors(cfg.color); + this.getColors(cfg.color ?? 'contrast'); } this.valueChange$ @@ -121,11 +121,12 @@ export class WidgetSliderComponent implements OnInit, OnDestroy { map((value) => { const cfg = this.runtime.options(); if (!cfg) return value; + const { lower, upper } = this.getScaleBounds(cfg); // Check if the value is within 1% of the lower or upper bounds - if (this.isWithinMargin(value, cfg.displayScale.lower, cfg)) { - return cfg.displayScale.lower; // Exact lower bound - } else if (this.isWithinMargin(value, cfg.displayScale.upper, cfg)) { - return cfg.displayScale.upper; // Exact upper bound + if (this.isWithinMargin(value, lower, cfg)) { + return lower; // Exact lower bound + } else if (this.isWithinMargin(value, upper, cfg)) { + return upper; // Exact upper bound } return parseFloat(value.toFixed(2)); // Round to 2 decimal places }), @@ -137,31 +138,42 @@ export class WidgetSliderComponent implements OnInit, OnDestroy { }); } + /** Real fallback matches DEFAULT_CONFIG.displayScale (lower: 0, upper: 1). */ + private getScaleBounds(cfg: IWidgetSvcConfig): { lower: number; upper: number } { + return { + lower: cfg.displayScale?.lower ?? 0, + upper: cfg.displayScale?.upper ?? 1 + }; + } + private mapValueToPosition(value: number): number { const cfg = this.runtime.options(); if (!cfg) return value; - const scaleRange = cfg.displayScale.upper - cfg.displayScale.lower; + const { lower, upper } = this.getScaleBounds(cfg); + const scaleRange = upper - lower; const lineRange = this.LINE_WIDTH; - return ((value - cfg.displayScale.lower) / scaleRange) * lineRange + this.LINE_START; + return ((value - lower) / scaleRange) * lineRange + this.LINE_START; } private mapPositionToValue(position: number): number { const cfg = this.runtime.options(); if (!cfg) return position; - const scaleRange = cfg.displayScale.upper - cfg.displayScale.lower; + const { lower, upper } = this.getScaleBounds(cfg); + const scaleRange = upper - lower; const lineRange = this.LINE_WIDTH; - return ((position - this.LINE_START) / lineRange) * scaleRange + cfg.displayScale.lower; + return ((position - this.LINE_START) / lineRange) * scaleRange + lower; } private isWithinMargin(value: number, target: number, cfg: IWidgetSvcConfig): boolean { - const margin = (cfg.displayScale.upper - cfg.displayScale.lower) * 0.01; // 1% margin + const { lower, upper } = this.getScaleBounds(cfg); + const margin = (upper - lower) * 0.01; // 1% margin return Math.abs(value - target) <= margin; } public sendValue(value: unknown): void { const path = this.runtime.options()?.paths?.['gaugePath']?.path; this.signalkRequestsService.putRequest( - path, + path ?? '', value, this.id() ); @@ -216,9 +228,9 @@ export class WidgetSliderComponent implements OnInit, OnDestroy { const cfg = this.runtime.options(); if (cfg && handlePosition <= this.LINE_START) { - this.pathValue = cfg.displayScale.lower; // Exact lower bound + this.pathValue = this.getScaleBounds(cfg).lower; // Exact lower bound } else if (cfg && handlePosition >= this.LINE_START + this.LINE_WIDTH) { - this.pathValue = cfg.displayScale.upper; // Exact upper bound + this.pathValue = this.getScaleBounds(cfg).upper; // Exact upper bound } else { this.pathValue = this.mapPositionToValue(handlePosition); } @@ -249,6 +261,7 @@ export class WidgetSliderComponent implements OnInit, OnDestroy { this.ensureColorMap(); if (!this.colorMap) return; const colors = this.colorMap.get(color) || this.colorMap.get("contrast"); + if (!colors) return; this.labelColor.set(colors.label); this.barColor.set(colors.bar); }