diff --git a/src/app/widget-config/graph-data-options/graph-data-options.component.html b/src/app/widget-config/graph-data-options/graph-data-options.component.html index 7f98e9c4..b7e476cc 100644 --- a/src/app/widget-config/graph-data-options/graph-data-options.component.html +++ b/src/app/widget-config/graph-data-options/graph-data-options.component.html @@ -62,7 +62,7 @@ - @if (datachartAngleRange(); as angleRangeCtrl) { + @if (angleRangeControl(); as angleRangeCtrl) { Angle display range @@ -70,7 +70,7 @@ Signed (-180° to 180°) Compass (0° to 360°) - For angular paths. Choose Signed for values that go negative (e.g. wind shift). + Choose Signed for values that go negative (e.g. wind shift). }
diff --git a/src/app/widget-config/graph-data-options/graph-data-options.component.spec.ts b/src/app/widget-config/graph-data-options/graph-data-options.component.spec.ts index 71eae3ee..b3155c69 100644 --- a/src/app/widget-config/graph-data-options/graph-data-options.component.spec.ts +++ b/src/app/widget-config/graph-data-options/graph-data-options.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { UntypedFormControl } from '@angular/forms'; +import { FormControl, UntypedFormControl } from '@angular/forms'; import { GraphDataOptionsComponent } from './graph-data-options.component'; import { DataService } from '../../core/services/data.service'; import { UnitsService } from '../../core/services/units.service'; @@ -13,9 +13,11 @@ describe('GraphDataOptionsComponent', () => { let component: GraphDataOptionsComponent; let fixture: ComponentFixture; let pathObject: Partial | null; + let pathUnits: Record; beforeEach(async () => { pathObject = null; + pathUnits = {}; await TestBed.configureTestingModule({ imports: [GraphDataOptionsComponent], providers: [ @@ -24,6 +26,7 @@ describe('GraphDataOptionsComponent', () => { useValue: { getPathsAndMetaByType: () => [], getPathObject: () => pathObject, + getPathUnitType: (path: string) => pathUnits[path] ?? null, }, }, { @@ -172,4 +175,58 @@ describe('GraphDataOptionsComponent', () => { vi.useRealTimers(); } }); + + const mountWithAngleRange = (path: string) => { + const fx = TestBed.createComponent(GraphDataOptionsComponent); + const set = fx.componentRef.setInput.bind(fx.componentRef) as (k: string, v: unknown) => void; + set('filterSelfPaths', new UntypedFormControl(false)); + set('datachartPath', new UntypedFormControl(path)); + set('datachartSource', new UntypedFormControl({ value: '', disabled: true })); + set('datachartAngleRange', new FormControl<'signed' | 'direction' | null>(null)); + set('timeScale', new UntypedFormControl('')); + set('period', new UntypedFormControl('')); + fx.detectChanges(); + return fx; + }; + + const showsAngleRange = (fx: ComponentFixture) => + ((fx.nativeElement as HTMLElement).textContent ?? '').includes('Angle display range'); + + it('hides the angle range for a path with a non-angular unit (#368)', () => { + pathUnits['self.propulsion.port.temperature'] = 'K'; + expect(showsAngleRange(mountWithAngleRange('self.propulsion.port.temperature'))).toBe(false); + }); + + it('offers the angle range for a radian path (#368)', () => { + pathUnits['self.environment.wind.angleApparent'] = 'rad'; + expect(showsAngleRange(mountWithAngleRange('self.environment.wind.angleApparent'))).toBe(true); + }); + + it('offers the angle range while the path publishes no unit (#368)', () => { + // An idle instrument publishes no metadata, and the override is then the only way to keep a + // graph angular — see resolveAngleDomain. + expect(showsAngleRange(mountWithAngleRange('self.environment.wind.angleApparent'))).toBe(true); + }); + + it('hides the angle range as soon as the path changes to a non-angular one (#368)', async () => { + // Behind the 300 ms path debounce the control would stay editable for a path it does not + // belong to. + vi.useFakeTimers(); + try { + pathUnits['self.environment.wind.angleApparent'] = 'rad'; + pathUnits['self.propulsion.port.temperature'] = 'K'; + const fx = mountWithAngleRange('self.environment.wind.angleApparent'); + expect(showsAngleRange(fx)).toBe(true); + + fx.componentInstance.datachartPath().setValue('self.propulsion.port.temperature'); + fx.detectChanges(); + expect(showsAngleRange(fx)).toBe(false); + + await vi.advanceTimersByTimeAsync(400); + fx.detectChanges(); + expect(showsAngleRange(fx)).toBe(false); + } finally { + vi.useRealTimers(); + } + }); }); diff --git a/src/app/widget-config/graph-data-options/graph-data-options.component.ts b/src/app/widget-config/graph-data-options/graph-data-options.component.ts index 6e814a46..dcf4577f 100644 --- a/src/app/widget-config/graph-data-options/graph-data-options.component.ts +++ b/src/app/widget-config/graph-data-options/graph-data-options.component.ts @@ -10,7 +10,7 @@ import { DataService } from '../../core/services/data.service'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatInputModule } from '@angular/material/input'; import { IPathMetaData, ISkPathData } from '../../core/interfaces/app-interfaces'; -import { debounceTime } from 'rxjs'; +import { debounceTime, tap } from 'rxjs'; import { RouterLink } from '@angular/router'; import { pathRequiredValidator, pathSlotWarning } from '../../core/utils/path-validators.util'; @@ -38,13 +38,33 @@ export class GraphDataOptionsComponent implements OnInit { protected pathWarning = signal(null); /** The path `pathSources` was last built for, so re-deriving needs a real path change. */ private _sourcesForPath: string | null = null; + /** Base unit of the selected path, or null when the server publishes none for it. */ + private pathUnit = signal(null); protected maxDuration = computed(() => this.timeScale().value === 'day' ? 365 : 60); + /** + * The angle range only changes how radian values are read, so it is offered for radian paths and + * withheld from every path with another unit. An unknown unit keeps it: metadata is missing while + * the producing instrument is idle, and the override is then the only way to keep the graph + * angular (see `resolveAngleDomain`). + */ + protected angleRangeControl = computed | undefined>(() => { + const control = this.datachartAngleRange(); + if (!control) return undefined; + const unit = this.pathUnit(); + return unit === null || unit === 'rad' ? control : undefined; + }); ngOnInit(): void { this.refreshNumericPaths(); this.filteredNumericPaths.set(this.numericPaths()); - this.datachartPath().valueChanges.pipe(debounceTime(300), takeUntilDestroyed(this._destroyRef)).subscribe(value => { + // The unit gates whether the angle range is shown at all, so it is read ahead of the debounce: + // behind it the control stays editable for 300 ms on a path it does not belong to. + this.datachartPath().valueChanges.pipe( + tap(value => this.refreshPathUnit(value)), + debounceTime(300), + takeUntilDestroyed(this._destroyRef) + ).subscribe(value => { this.refreshNumericPaths(); const term = (value || '').toLowerCase().trim(); if (!term) { @@ -66,6 +86,7 @@ export class GraphDataOptionsComponent implements OnInit { this.datachartPath().updateValueAndValidity({ emitEvent: false }); const currentPath = this.datachartPath()?.value; this.refreshPathWarning(currentPath); + this.refreshPathUnit(currentPath); this.setPathSourcesFor(currentPath); this.setInitFormState(); } @@ -80,6 +101,10 @@ export class GraphDataOptionsComponent implements OnInit { })); } + private refreshPathUnit(path: string | null): void { + this.pathUnit.set(path ? this.data.getPathUnitType(path) : null); + } + /** * Build the Source list for `path`, keeping the select usable when the server is not sending that * path: its sources are unknown, so surface the stored one alongside "Any" rather than leaving an