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