diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/app.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/app.component.html
index cc96faf4863cc775939491acf27205571d0e7dd1..bc7ed586bdd1d2a93cfd116180b1f526570d66d5 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/app.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/app.component.html
@@ -18,12 +18,12 @@
 
   <!-- Provisional model warning -->
   <nshmp-lib-ng-hazard-provisional-model
-    [name]="(facade.usageModelA$ | async)?.response?.model?.name"
+    [name]="facade.usageModelA()?.response?.model?.name"
   />
 
   <!-- Provisional model warning -->
   <nshmp-lib-ng-hazard-provisional-model
-    [name]="(facade.usageModelB$ | async)?.response?.model?.name"
+    [name]="facade.usageModelB()?.response?.model?.name"
   />
 
   <!-- About page -->
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.html
index 66789e92d33da64912291468d6bb9eccfcbc8468..cc0344672b48a9bc32f16ed24f197f5583b75f03 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.html
@@ -7,17 +7,14 @@
   </mat-tab>
 
   <!-- Data tab -->
-  <mat-tab label="Hazard Curve Data" [disabled]="(hasData$ | async) === false">
+  <mat-tab label="Hazard Curve Data" [disabled]="hasData() === false">
     <ng-template matTabContent>
       <app-hazard-data />
     </ng-template>
   </mat-tab>
 
   <!-- Spectra tab -->
-  <mat-tab
-    label="Response Spectrum Data"
-    [disabled]="(hasData$ | async) === false"
-  >
+  <mat-tab label="Response Spectrum Data" [disabled]="hasData() === false">
     <ng-template matTabContent>
       <app-spectra-data />
     </ng-template>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.ts
index 895cf79bb35adcb91fcf64d540b15d810474582d..d66be9f4bbe2c02e7ad7bb2621c82e7e0afd7161 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.ts
@@ -1,7 +1,6 @@
 import {AsyncPipe} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed} from '@angular/core';
 import {MatTab, MatTabContent, MatTabGroup} from '@angular/material/tabs';
-import {map} from 'rxjs';
 
 import {AppFacade} from '../../state/app.facade';
 import {HazardDataComponent} from '../hazard-data/hazard-data.component';
@@ -31,13 +30,14 @@ import {SpectraDataComponent} from '../spectra-data/spectra-data.component';
 })
 export class ContentComponent {
   /** Whether there is data */
-  hasData$ = this.facade.serviceResponses$.pipe(
-    map(
-      models =>
-        models.modelA?.hazardResponse !== undefined &&
-        models.modelB?.hazardResponse !== undefined
-    )
-  );
+  hasData = computed(() => {
+    const {modelA, modelB} = this.facade.serviceResponses();
+
+    return (
+      modelA?.hazardResponse !== undefined &&
+      modelB?.hazardResponse !== undefined
+    );
+  });
 
   constructor(private facade: AppFacade) {}
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.html
index 3bf9b85de0912e3c6704cfd4307b8cf381e37220..a1a3b1e134375a2639fbd258fc3d0b4f027bef89 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.html
@@ -1,100 +1,95 @@
 <!-- Control Panel -->
-@if (formState$ | async; as formState) {
-  <form
-    class="height-full overflow-auto"
-    [ngrxFormState]="formState"
-    (submit)="onSubmit()"
-  >
-    <!-- Model -->
-    <nshmp-lib-ng-hazard-model-form
-      [controlState]="formState?.controls?.model"
-      [models]="facade.availableModels$ | async"
-    />
+<form
+  class="height-full overflow-auto"
+  [formGroup]="formGroup"
+  (submit)="onSubmit()"
+>
+  <!-- Model -->
+  <nshmp-lib-no-ngrx-hazard-model-form
+    [modelControl]="formGroup.controls.model"
+    [models]="facade.availableModels()"
+  />
 
-    <!-- Model Compare -->
-    <nshmp-lib-ng-hazard-model-form
-      [controlState]="formState?.controls?.modelCompare"
-      [models]="facade.comparableModels$ | async"
-      label="Model Compare"
-      placeHolderText="--- Select a Different Model ---"
-    />
+  <!-- Model Compare -->
+  <nshmp-lib-no-ngrx-hazard-model-form
+    [modelControl]="formGroup.controls.modelCompare"
+    [models]="facade.comparableModels()"
+    label="Model Compare"
+    placeHolderText="--- Select a Different Model ---"
+  />
 
+  @if (facade.usage()) {
     <!-- Latitude -->
-    <nshmp-lib-ng-hazard-location-form
+    <nshmp-lib-no-ngrx-hazard-location-form
       class="latitude-input"
-      [controlState]="formState?.controls?.latitude"
+      [locationControl]="formGroup.controls.latitude"
+      [modelControl]="formGroup.controls.model"
       label="Latitude"
-      [bounds]="(facade.usage$ | async)?.response?.latitude"
+      [bounds]="facade.usage().response.latitude"
     />
 
     <!-- Longitude -->
-    <nshmp-lib-ng-hazard-location-form
+    <nshmp-lib-no-ngrx-hazard-location-form
       class="longitude-input"
-      [controlState]="formState?.controls?.longitude"
+      [locationControl]="formGroup.controls.longitude"
+      [modelControl]="formGroup.controls.model"
       label="Longitude"
-      [bounds]="(facade.usage$ | async)?.response?.longitude"
+      [bounds]="facade.usage().response.longitude"
     />
+  }
 
-    <!-- Select site -->
-    <nshmp-lib-ng-map-select-site
-      [data]="selectSiteData$ | async"
-      (siteSelect)="facade.setLocation($event)"
-    />
+  <!-- Select site -->
+  <nshmp-lib-no-ngrx-map-select-site
+    [data]="selectSiteData()"
+    (siteSelect)="facade.setLocation($event)"
+  />
 
-    <!-- Imt -->
-    <mat-form-field class="grid-col-12">
-      <mat-label>Intensity Measure Type</mat-label>
-      <mat-select [ngrxFormControlState]="formState?.controls?.imt">
-        @for (imt of imts$ | async; track imt) {
-          <mat-option [value]="imt.value">
-            {{ imt.display }}
-          </mat-option>
-        }
-      </mat-select>
-    </mat-form-field>
+  <!-- Imt -->
+  <mat-form-field class="grid-col-12">
+    <mat-label>Intensity Measure Type</mat-label>
+    <mat-select [formControl]="formGroup.controls.imt">
+      @for (imt of imts(); track imt) {
+        <mat-option [value]="imt.value">
+          {{ imt.display }}
+        </mat-option>
+      }
+    </mat-select>
+  </mat-form-field>
 
-    <!-- Site Class -->
-    <div class="grid-row grid-gap-sm">
-      <!-- Site classes -->
-      <nshmp-lib-ng-hazard-site-class-form
-        class="grid-col-7"
-        [controlState]="formState?.controls?.siteClass"
-        [siteClasses]="siteClasses$ | async"
-        formFieldClass="grid-col-12"
+  <!-- Site Class -->
+  <div class="grid-row grid-gap-sm">
+    <!-- Site classes -->
+    @if (siteClasses()) {
+      <nshmp-lib-no-ngrx-hazard-site-class-form
+        [siteClassControl]="formGroup.controls.siteClass"
+        [siteClasses]="siteClasses()"
+        [modelControl]="formGroup.controls.model"
+        [vs30Control]="formGroup.controls.vs30"
+        [vs30Bounds]="facade.usage()?.response.vs30"
       />
+    }
+  </div>
 
-      <!-- Vs30-->
-      <nshmp-lib-ng-hazard-vs30-form
-        class="grid-col-5"
-        [controlState]="formState?.controls?.vs30"
-        [bounds]="(facade.usage$ | async)?.vs30"
-        formFieldClass="grid-col-12"
-      />
-    </div>
+  <!-- Return Period Controls -->
+  <nshmp-lib-no-ngrx-hazard-return-period-form
+    [returnPeriodControl]="formGroup.controls.returnPeriod"
+    [commonReturnPeriodControl]="formGroup.controls.commonReturnPeriods"
+  />
 
-    <!-- Return Period Controls -->
-    <nshmp-lib-ng-hazard-return-period-form
-      [returnPeriodControlState]="formState?.controls?.returnPeriod"
-      [commonReturnPeriodControlState]="
-        formState?.controls?.commonReturnPeriods
-      "
-    />
+  <!-- Truncation -->
+  <nshmp-lib-no-ngrx-hazard-truncation-form
+    [truncationControl]="formGroup.controls.truncate"
+  />
 
-    <!-- Truncation -->
-    <nshmp-lib-ng-hazard-truncation-form
-      [controlState]="formState?.controls?.truncate"
-    />
-
-    <!-- Max Direction -->
-    <nshmp-lib-ng-hazard-max-direction-form
-      [controlState]="formState?.controls?.maxDirection"
-    />
+  <!-- Max Direction -->
+  <nshmp-lib-no-ngrx-hazard-max-direction-form
+    [maxDirectionControl]="formGroup.controls.maxDirection"
+  />
 
-    <nshmp-lib-ng-control-panel-buttons
-      [plotDisabled]="formState?.isInvalid"
-      [serviceCallInfo]="facade.serviceCallInfo$ | async"
-      [resetDisabled]="formState?.isPristine"
-      (resetClick)="facade.resetControlPanel()"
-    />
-  </form>
-}
+  <nshmp-lib-no-ngrx-control-panel-buttons
+    [plotDisabled]="formGroup.invalid"
+    [serviceCallInfo]="facade.serviceCallInfo()"
+    [resetDisabled]="formGroup.pristine"
+    (resetClick)="facade.resetControlPanel()"
+  />
+</form>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.ts
index 1af22eebf94b378d005286ed8aa6ea3e60343e67..95f23833f37df2a2825e4d64685f7058f11fbd3a 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.ts
@@ -1,34 +1,35 @@
 import {AsyncPipe} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed, OnDestroy, OnInit, Signal} from '@angular/core';
+import {ReactiveFormsModule} from '@angular/forms';
 import {MatOption} from '@angular/material/core';
-import {MatFormField, MatLabel} from '@angular/material/form-field';
+import {MatError, MatFormField, MatLabel} from '@angular/material/form-field';
+import {MatInput} from '@angular/material/input';
 import {MatSelect} from '@angular/material/select';
 import {
-  hazardUtils,
   NshmpLibNgHazardLocationFormComponent,
   NshmpLibNgHazardMaxDirectionFormComponent,
   NshmpLibNgHazardModelFormComponent,
   NshmpLibNgHazardReturnPeriodFormComponent,
   NshmpLibNgHazardSiteClassFormComponent,
   NshmpLibNgHazardTruncationFormComponent,
-  NshmpLibNgHazardVs30FormComponent,
-} from '@ghsc/nshmp-lib-ng/hazard';
+} from '@ghsc/nshmp-lib-no-ngrx/hazard';
 import {
   NshmpLibNgMapSelectSiteComponent,
   SelectSiteDialogData,
-} from '@ghsc/nshmp-lib-ng/map';
+} from '@ghsc/nshmp-lib-no-ngrx/map';
 import {
   NshmpLibNgControlPanelButtonsComponent,
-  NshmpNgrxFormsModule,
   NshmpService,
   nshmpUtils,
   RETURN_PERIOD_BOUNDS,
   RETURN_PERIODS,
-} from '@ghsc/nshmp-lib-ng/nshmp';
+} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
 import {environment} from 'projects/nshmp-apps/src/environments/environment';
-import {combineLatest, map, Observable} from 'rxjs';
+import {Subscription} from 'rxjs';
 
 import {AppFacade} from '../../state/app.facade';
+import {combineUsages} from '../../utils/response-handler.utils';
 
 /**
  * Control panel with form fields to call dynamic hazard services.
@@ -40,23 +41,24 @@ import {AppFacade} from '../../state/app.facade';
     MatSelect,
     MatOption,
     AsyncPipe,
+    MatInput,
     NshmpLibNgHazardModelFormComponent,
     NshmpLibNgHazardLocationFormComponent,
     NshmpLibNgMapSelectSiteComponent,
     NshmpLibNgHazardSiteClassFormComponent,
-    NshmpLibNgHazardVs30FormComponent,
     NshmpLibNgHazardReturnPeriodFormComponent,
     NshmpLibNgHazardTruncationFormComponent,
     NshmpLibNgHazardMaxDirectionFormComponent,
     NshmpLibNgControlPanelButtonsComponent,
-    NshmpNgrxFormsModule,
+    ReactiveFormsModule,
+    MatError,
   ],
   selector: 'app-control-panel',
   standalone: true,
   styleUrl: './control-panel.component.scss',
   templateUrl: './control-panel.component.html',
 })
-export class ControlPanelComponent {
+export class ControlPanelComponent implements OnInit, OnDestroy {
   /** Max and min bounds for return periods */
   returnPeriodBounds = RETURN_PERIOD_BOUNDS;
   /** List of return periods */
@@ -65,45 +67,85 @@ export class ControlPanelComponent {
   siteClassPlaceHolder = nshmpUtils.selectPlaceHolder();
 
   /** Form field state */
-  formState$ = this.facade.controlPanelForm$;
+  formGroup = this.facade.formGroup;
 
   /** List of available IMTs */
-  imts$ = this.facade.usage$.pipe(map(usage => usage?.response?.model?.imts));
+  imts = computed(() => this.facade.usage()?.response?.model?.imts);
 
   /** Data for select site component */
-  selectSiteData$: Observable<SelectSiteDialogData> = combineLatest([
-    this.facade.usage$,
-    this.facade.controlPanelForm$,
-    this.facade.nshmService$,
-  ]).pipe(
-    map(([usage, form, nshmService]) => {
-      if (usage === null || nshmService === undefined) {
-        return;
-      }
-
-      const services =
-        environment.webServices.nshmpHazWs.services.curveServices;
-
-      return {
-        mapUrl: `${nshmService.url}${services.map}?raw=true`,
-        nshm: form.value.model,
-        sitesUrl: `${nshmService.url}${services.sites}?raw=true`,
-      };
-    })
-  );
+  selectSiteData: Signal<SelectSiteDialogData> = computed(() => {
+    const usage = this.facade.usage();
+    const nshmService = this.facade.nshmService();
+
+    if (usage === null || nshmService === undefined) {
+      return;
+    }
+
+    const services = environment.webServices.nshmpHazWs.services.curveServices;
+
+    return {
+      mapUrl: `${nshmService.url}${services.map}?raw=true`,
+      nshm: this.facade.formGroup.getRawValue().model,
+      sitesUrl: `${nshmService.url}${services.sites}?raw=true`,
+    };
+  });
 
   /** List of site class `Parameter`s */
-  siteClasses$ = this.facade.usage$.pipe(
-    map(usage =>
-      hazardUtils.siteClassesToParameters(usage?.response?.model?.siteClasses)
-    )
+  siteClasses = computed(
+    () => this.facade.usage()?.response?.model.siteClasses
   );
 
+  private imtSubscription = new Subscription();
+  private maxDirectionSubscription = new Subscription();
+  private modelCompareSubscription = new Subscription();
+  private modelSubscription = new Subscription();
+  private truncateSubscription = new Subscription();
+  private vs30Subscription = new Subscription();
+
   constructor(
     public facade: AppFacade,
     private nshmpService: NshmpService
   ) {}
 
+  ngOnInit(): void {
+    this.modelSubscription =
+      this.formGroup.controls.model.valueChanges.subscribe(() => {
+        this.facade.onModelChange();
+      });
+
+    this.modelCompareSubscription =
+      this.formGroup.controls.modelCompare.valueChanges.subscribe(
+        modelCompare => this.onModelCompare(modelCompare)
+      );
+
+    this.imtSubscription = this.formGroup.controls.imt.valueChanges.subscribe(
+      () => this.facade.createPlots()
+    );
+
+    this.maxDirectionSubscription =
+      this.formGroup.controls.maxDirection.valueChanges.subscribe(() =>
+        this.facade.createPlots()
+      );
+
+    this.truncateSubscription =
+      this.formGroup.controls.truncate.valueChanges.subscribe(() =>
+        this.facade.createPlots()
+      );
+
+    this.vs30Subscription = this.formGroup.controls.vs30.valueChanges.subscribe(
+      () => this.facade.resetState()
+    );
+  }
+
+  ngOnDestroy(): void {
+    this.imtSubscription.unsubscribe();
+    this.maxDirectionSubscription.unsubscribe();
+    this.modelCompareSubscription.unsubscribe();
+    this.modelSubscription.unsubscribe();
+    this.truncateSubscription.unsubscribe();
+    this.vs30Subscription.unsubscribe();
+  }
+
   /**
    * On form submit.
    */
@@ -111,4 +153,16 @@ export class ControlPanelComponent {
     this.facade.callServices();
     this.nshmpService.selectPlotControl();
   }
+
+  private onModelCompare(modelCompare: NshmId): void {
+    if (modelCompare === null) {
+      this.facade.onModelChange();
+    } else {
+      const usages = [this.facade.usageModelA(), this.facade.usageModelB()];
+
+      this.facade.updateState({
+        combinedUsage: combineUsages(usages),
+      });
+    }
+  }
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.html
index 16a791208cc5f59fc2c18df32a23339e39ed7e56..786dd8646e3c858c78d961d5fa37a3d4b7e3c287 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.html
@@ -1,20 +1,20 @@
 <div class="grid-container-widescreen-xl margin-y-3">
   <mat-accordion multi>
     <app-table-data-panel
-      [filename]="hazardFilename$ | async"
-      [tableData]="hazardTableData$ | async"
+      [filename]="hazardFilename"
+      [tableData]="hazardTableData()"
       panelTitle="Hazard Curves: Total"
     />
 
     <app-table-data-panel
-      [filename]="hazardDiffFilename$ | async"
-      [tableData]="hazardDiffTableData$ | async"
+      [filename]="hazardDiffFilename"
+      [tableData]="hazardDiffTableData()"
       panelTitle="Hazard Curves: Percent Difference"
     />
 
     <app-table-data-panel
-      [filename]="hazardComponentsFilename$ | async"
-      [tableData]="hazardComponentsTableData$ | async"
+      [filename]="hazardComponentsFilename"
+      [tableData]="hazardComponentsTableData()"
       panelTitle="Hazard Curves: Components"
     />
   </mat-accordion>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.ts
index 7f1bc6575be0f83b2d0436d23805faddd0b6746d..623e587be6e87934a50c515428569d63fc3854ac 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.ts
@@ -1,10 +1,9 @@
 import {AsyncPipe} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed} from '@angular/core';
 import {MatAccordion} from '@angular/material/expansion';
-import {PlotTableDataParams, plotUtils} from '@ghsc/nshmp-lib-ng/plot';
+import {PlotTableDataParams, plotUtils} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {Imt, imtToString} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
 import {PlotData} from 'plotly.js';
-import {map} from 'rxjs';
 
 import {AppFacade} from '../../state/app.facade';
 import {Plots} from '../../state/app.state';
@@ -22,19 +21,15 @@ import {TableDataPanelComponent} from '../table-data-panel/table-data-panel.comp
 })
 export class HazardDataComponent {
   /** Hazard components plot data */
-  private hazardComponentsPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD_COMPONENTS))
-  );
+  private hazardComponentsPlot = this.facade
+    .plots()
+    .get(Plots.HAZARD_COMPONENTS);
 
   /** Hazard % diff plot data */
-  private hazardDiffPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD_DIFFERENCES))
-  );
+  private hazardDiffPlot = this.facade.plots().get(Plots.HAZARD_DIFFERENCES);
 
   /** Hazard plot data */
-  private hazardPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD))
-  );
+  private hazardPlot = this.facade.plots().get(Plots.HAZARD);
 
   /** Table data parameters */
   private tableParams: PlotTableDataParams = {
@@ -49,71 +44,57 @@ export class HazardDataComponent {
   };
 
   /** Model values */
-  private models$ = this.facade.controlPanelForm$.pipe(
-    map(form => `${form.value.model}-${form.value.modelCompare}`)
-  );
+  private models = `${this.facade.formGroup.getRawValue().model}-${this.facade.formGroup.getRawValue().modelCompare}`;
 
   /** Filename for hazard components export CSV */
-  hazardComponentsFilename$ = this.models$.pipe(
-    map(models => `hazard-components-${models}.csv`)
-  );
+  hazardComponentsFilename = `hazard-components-${this.models}.csv`;
 
   /** Filename for hazard export CSV */
-  hazardFilename$ = this.models$.pipe(
-    map(models => `hazard-compare-${models}.csv`)
-  );
+  hazardFilename = `hazard-compare-${this.models}.csv`;
 
   /** Filename for hazard difference export CSV */
-  hazardDiffFilename$ = this.models$.pipe(
-    map(models => `hazard-%-diff-${models}.csv`)
-  );
+  hazardDiffFilename = `hazard-%-diff-${this.models}.csv`;
 
   /** Hazard table data */
-  hazardTableData$ = this.hazardPlot$.pipe(
-    map(plot => {
-      const data = [...plot.plotData.data];
-      // Remove return period from data
-      data.shift();
+  hazardTableData = computed(() => {
+    const data = [...this.hazardPlot.plotData.data];
+    // Remove return period from data
+    data.shift();
 
-      return plotUtils.plotDataToTableData(
-        {
-          ...plot,
-          plotData: {
-            ...plot.plotData,
-            data,
-          },
+    return plotUtils.plotDataToTableData(
+      {
+        ...this.hazardPlot,
+        plotData: {
+          ...this.hazardPlot.plotData,
+          data,
         },
-        this.tableParams
-      );
-    })
-  );
+      },
+      this.tableParams
+    );
+  });
 
   /** Hazard difference table data */
-  hazardDiffTableData$ = this.hazardDiffPlot$.pipe(
-    map(plot => {
-      return plotUtils.plotDataToTableData(plot, this.tableParams);
-    })
+  hazardDiffTableData = computed(() =>
+    plotUtils.plotDataToTableData(this.hazardDiffPlot, this.tableParams)
   );
 
   /** Hazard components table data */
-  hazardComponentsTableData$ = this.hazardComponentsPlot$.pipe(
-    map(plot => {
-      const data = [...plot.plotData.data];
-      // Remove return period from data
-      data.shift();
+  hazardComponentsTableData = computed(() => {
+    const data = [...this.hazardComponentsPlot.plotData.data];
+    // Remove return period from data
+    data.shift();
 
-      return plotUtils.plotDataToTableData(
-        {
-          ...plot,
-          plotData: {
-            ...plot.plotData,
-            data,
-          },
+    return plotUtils.plotDataToTableData(
+      {
+        ...this.hazardComponentsPlot,
+        plotData: {
+          ...this.hazardComponentsPlot.plotData,
+          data,
         },
-        this.tableParams
-      );
-    })
-  );
+      },
+      this.tableParams
+    );
+  });
 
   constructor(private facade: AppFacade) {}
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.html
index 2b1c1476e38f3faa9f6d22a094303d7ba01c8565..4689f8b4e662c99e5375b0b8d8d6913720f0e80c 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.html
@@ -1,69 +1,65 @@
-@if (form$ | async; as form) {
-  <div class="grid-row parameter-summary">
-    <div class="grid-col-12 tablet:grid-col-6">
-      <mat-list class="parameter-list">
-        <mat-list-item>
-          <span class="parameter">Model</span>:
-          {{ toDisplay(form.value.model, facade.availableModels$ | async) }}
-        </mat-list-item>
+<div class="grid-row parameter-summary">
+  <div class="grid-col-12 tablet:grid-col-6">
+    <mat-list class="parameter-list">
+      <mat-list-item>
+        <span class="parameter">Model</span>:
+        {{ toDisplay(form.value.model, facade.availableModels()) }}
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Model Compare</span>:
-          {{
-            toDisplay(form.value.modelCompare, facade.availableModels$ | async)
-          }}
-        </mat-list-item>
+      <mat-list-item>
+        <span class="parameter">Model Compare</span>:
+        {{ toDisplay(form.value.modelCompare, facade.availableModels()) }}
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Latitude</span>: {{ form.value.latitude }}°
-        </mat-list-item>
+      <mat-list-item>
+        <span class="parameter">Latitude</span>: {{ form.value.latitude }}°
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Longitude</span>: {{ form.value.longitude }}°
-        </mat-list-item>
+      <mat-list-item>
+        <span class="parameter">Longitude</span>: {{ form.value.longitude }}°
+      </mat-list-item>
 
-        <mat-list-item>
-          @if (form.value.siteClass !== selectPlaceHolder.value) {
-            <span class="parameter">Site Class</span>:
-            {{ toDisplay(form.value.siteClass, siteClasses$ | async) }}
-          } @else {
-            <span class="parameter">Vs30</span>: {{ form.value.vs30 }} m/s
-          }
-        </mat-list-item>
-      </mat-list>
-    </div>
+      <mat-list-item>
+        @if (form.value.siteClass !== selectPlaceHolder.value) {
+          <span class="parameter">Site Class</span>:
+          {{ toDisplay(form.value.siteClass, siteClasses$ | async) }}
+        } @else {
+          <span class="parameter">Vs30</span>: {{ form.value.vs30 }} m/s
+        }
+      </mat-list-item>
+    </mat-list>
+  </div>
 
-    <div class="grid-col-12 tablet:grid-col-6 print-flex-basis-half">
-      <mat-list class="parameter-list">
-        <mat-list-item>
-          <span class="parameter">IMT</span>:
-          {{ form.value.imt }}
-        </mat-list-item>
+  <div class="grid-col-12 tablet:grid-col-6 print-flex-basis-half">
+    <mat-list class="parameter-list">
+      <mat-list-item>
+        <span class="parameter">IMT</span>:
+        {{ form.value.imt }}
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Return Period</span>:
-          @if (form.value.commonReturnPeriod !== selectPlaceHolder.value) {
-            {{ returnPeriodName[form.value.returnPeriod] }}
-          } @else {
-            {{ form.value.returnPeriod }} yr
-          }
-        </mat-list-item>
+      <mat-list-item>
+        <span class="parameter">Return Period</span>:
+        <!-- @if (form.getRawValue().returnPeriod !== selectPlaceHolder.value) {
+          {{ returnPeriodName[form.value.returnPeriod] }}
+        } @else {
+          {{ form.value.returnPeriod }} yr
+        } -->
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Source Type</span>:
-          {{ form.value.sourceType }}
-        </mat-list-item>
+      <mat-list-item>
+        <span class="parameter">Source Type</span>:
+        {{ form.value.sourceType }}
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Truncate</span>:
-          {{ form.value.truncate === true ? 'On' : 'Off' }}
-        </mat-list-item>
+      <mat-list-item>
+        <span class="parameter">Truncate</span>:
+        {{ form.value.truncate === true ? 'On' : 'Off' }}
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Max Direction</span>:
-          {{ form.value.maxDirection === true ? 'On' : 'Off' }}
-        </mat-list-item>
-      </mat-list>
-    </div>
+      <mat-list-item>
+        <span class="parameter">Max Direction</span>:
+        {{ form.value.maxDirection === true ? 'On' : 'Off' }}
+      </mat-list-item>
+    </mat-list>
   </div>
-}
+</div>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.ts
index 9ed74b4711ccf5a74a39e008f1d0f86562a04261..17c3004aeefc4ca6c4a27250e027e4e89ed91e68 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.ts
@@ -1,9 +1,8 @@
 import {AsyncPipe} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed} from '@angular/core';
 import {MatList, MatListItem} from '@angular/material/list';
-import {hazardUtils} from '@ghsc/nshmp-lib-ng/hazard';
-import {nshmpUtils, returnPeriodAltName} from '@ghsc/nshmp-lib-ng/nshmp';
-import {map} from 'rxjs';
+import {hazardUtils} from '@ghsc/nshmp-lib-no-ngrx/hazard';
+import {nshmpUtils, returnPeriodAltName} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
 
 import {AppFacade} from '../../state/app.facade';
 
@@ -25,14 +24,20 @@ export class ParameterSummaryComponent {
   toDisplay = nshmpUtils.parameterToDisplay;
 
   /** Form field state */
-  form$ = this.facade.controlPanelForm$;
+  form = this.facade.formGroup;
 
   /** Site classes from usage as `Parameter`s */
-  siteClasses$ = this.facade.usage$.pipe(
-    map(usage =>
-      hazardUtils.siteClassesToParameters(usage?.response?.model?.siteClasses)
-    )
-  );
+  siteClasses = computed(() => {
+    const usage = this.facade.usage();
 
-  constructor(private facade: AppFacade) {}
+    if (usage === undefined || usage === null) {
+      return [];
+    }
+
+    return hazardUtils.siteClassesToParameters(
+      usage.response.model.siteClasses
+    );
+  });
+
+  constructor(public facade: AppFacade) {}
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.html
index 31daed70807f3d906515b047eb2163bae00dd805..8dfad0ad328f5207a5aa9ecface99524146296a5 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.html
@@ -5,7 +5,10 @@
         <mat-expansion-panel-header>
           <mat-panel-title>{{ panel.title }}</mat-panel-title>
         </mat-expansion-panel-header>
-        <nshmp-lib-ng-plot-settings [plot]="panel.plot$ | async" />
+        <nshmp-lib-no-ngrx-plot-settings
+          [plot]="panel.plot()"
+          (updatedPlot)="updatePlot(panel.id, $event)"
+        />
       </mat-expansion-panel>
     }
   </mat-accordion>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.ts
index 6156988a8fd66e4d091b81b5be994a4ac53a6d74..4b262db0941e5e8d02d85422f22fed00cc5c2dda 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.ts
@@ -1,5 +1,5 @@
 import {AsyncPipe} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed} from '@angular/core';
 import {
   MatAccordion,
   MatExpansionPanel,
@@ -8,9 +8,9 @@ import {
 } from '@angular/material/expansion';
 import {
   NshmpLibNgPlotSettingsComponent,
-  PlotSettingsPanel,
-} from '@ghsc/nshmp-lib-ng/plot';
-import {map} from 'rxjs';
+  NshmpPlot,
+} from '@ghsc/nshmp-lib-no-ngrx/plot';
+import {PlotSettingsPanelId} from 'projects/nshmp-apps/src/shared/models/plot-settings-panel.model';
 
 import {AppFacade} from '../../state/app.facade';
 import {Plots} from '../../state/app.state';
@@ -34,62 +34,73 @@ import {Plots} from '../../state/app.state';
 })
 export class PlotSettingsComponent {
   /** Hazard plot data */
-  private hazardPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD))
-  );
+  private hazardPlot = computed(() => this.facade.plots().get(Plots.HAZARD));
 
   /** Hazard components plot data */
-  private hazardComponentsPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD_COMPONENTS))
+  private hazardComponentsPlot = computed(() =>
+    this.facade.plots().get(Plots.HAZARD_COMPONENTS)
   );
 
   /** Hazard % diff plot data */
-  private hazardDiffPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD_DIFFERENCES))
+  private hazardDiffPlot = computed(() =>
+    this.facade.plots().get(Plots.HAZARD_DIFFERENCES)
   );
 
   /** Spectra components plot data */
-  private spectraComponentsPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.SPECTRUM_COMPONENTS))
+  private spectraComponentsPlot = computed(() =>
+    this.facade.plots().get(Plots.SPECTRUM_COMPONENTS)
   );
 
   /** Spectra % diff plot data */
-  private spectraDiffPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.SPECTRUM_DIFFERENCES))
+  private spectraDiffPlot = computed(() =>
+    this.facade.plots().get(Plots.SPECTRUM_DIFFERENCES)
   );
 
   /** Spectra plot data */
-  private spectraPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.SPECTRUM))
-  );
+  private spectraPlot = computed(() => this.facade.plots().get(Plots.SPECTRUM));
 
   /** The panels with plot settings */
-  panels: PlotSettingsPanel[] = [
+  panels: PlotSettingsPanelId[] = [
     {
-      plot$: this.hazardPlot$,
+      id: Plots.HAZARD,
+      plot: this.hazardPlot,
       title: 'Hazard Curves Plot Settings',
     },
     {
-      plot$: this.hazardDiffPlot$,
+      id: Plots.HAZARD_DIFFERENCES,
+      plot: this.hazardDiffPlot,
       title: 'Hazard % Difference Plot Settings',
     },
     {
-      plot$: this.spectraPlot$,
+      id: Plots.SPECTRUM,
+      plot: this.spectraPlot,
       title: 'Spectra Plot Settings',
     },
     {
-      plot$: this.spectraDiffPlot$,
+      id: Plots.SPECTRUM_DIFFERENCES,
+      plot: this.spectraDiffPlot,
       title: 'Spectra % Difference Plot Settings',
     },
     {
-      plot$: this.hazardComponentsPlot$,
+      id: Plots.HAZARD_COMPONENTS,
+      plot: this.hazardComponentsPlot,
       title: 'Hazard Component Curves Plot Settings',
     },
     {
-      plot$: this.spectraComponentsPlot$,
+      id: Plots.SPECTRUM_COMPONENTS,
+      plot: this.spectraComponentsPlot,
       title: 'Spectra Component Curves Plot Settings',
     },
   ];
 
   constructor(private facade: AppFacade) {}
+
+  updatePlot(id: string, plot: NshmpPlot): void {
+    console.log(id, plot);
+    const plots = new Map(this.facade.state().plots);
+    plots.set(id, plot);
+    this.facade.updateState({
+      plots,
+    });
+  }
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.html
index 184bd1f0660fb3885f5ec700efe7616fe15b1f06..256fa41fc70374bc3503878e0a0ebb16a660da5b 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.html
@@ -1,5 +1,7 @@
-<nshmp-lib-ng-plots-container containerClass="grid-container-widescreen-xl">
-  <nshmp-lib-ng-hazard-design-code-warning />
+<nshmp-lib-no-ngrx-plots-container
+  containerClass="grid-container-widescreen-xl"
+>
+  <nshmp-lib-no-ngrx-hazard-design-code-warning />
 
   <div class="grid-row">
     @for (plot of plots; track plot) {
@@ -16,13 +18,13 @@
               <mat-panel-title>{{ plot.title }}</mat-panel-title>
             </mat-expansion-panel-header>
             <mat-divider />
-            @if (plot.plotData$ | async; as plotData) {
-              <nshmp-lib-ng-plot [plot]="plotData" />
-            }
-            @if (plot.diffPlotData$ | async; as diffPlotData) {
+
+            <nshmp-lib-no-ngrx-plot [plot]="plot.plotData()" />
+
+            @if (plot.diffPlotData) {
               <div class="padding-top-4">
                 <mat-divider />
-                <nshmp-lib-ng-plot [plot]="diffPlotData" />
+                <nshmp-lib-no-ngrx-plot [plot]="plot.diffPlotData()" />
               </div>
             }
           </mat-expansion-panel>
@@ -49,9 +51,9 @@
           </mat-expansion-panel-header>
 
           <mat-divider />
-          <nshmp-lib-ng-app-metadata [repositories]="repositories$ | async" />
+          <nshmp-lib-no-ngrx-app-metadata [repositories]="repositories()" />
         </mat-expansion-panel>
       </mat-accordion>
     </div>
   </div>
-</nshmp-lib-ng-plots-container>
+</nshmp-lib-no-ngrx-plots-container>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.ts
index 206fc18f6edc1a9ee46040af196ffea67a1bf748..89210f5d8fdaab491f0a0c290b29197e9664897d 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.ts
@@ -1,5 +1,5 @@
 import {AsyncPipe, NgClass} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed, Signal} from '@angular/core';
 import {MatDivider} from '@angular/material/divider';
 import {
   MatAccordion,
@@ -7,15 +7,15 @@ import {
   MatExpansionPanelHeader,
   MatExpansionPanelTitle,
 } from '@angular/material/expansion';
-import {NshmpLibNgHazardDesignCodeWarningComponent} from '@ghsc/nshmp-lib-ng/hazard';
-import {NshmpLibNgAppMetadataComponent} from '@ghsc/nshmp-lib-ng/nshmp';
+import {NshmpLibNgHazardDesignCodeWarningComponent} from '@ghsc/nshmp-lib-no-ngrx/hazard';
+import {NshmpLibNgAppMetadataComponent} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
 import {
   NshmpLibNgPlotComponent,
   NshmpLibNgPlotsContainerComponent,
-} from '@ghsc/nshmp-lib-ng/plot';
+} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {AppControlsService} from '@ghsc/nshmp-template';
 import {PlotlyPlot} from '@ghsc/nshmp-utils-ts/libs/plotly';
-import {map, Observable} from 'rxjs';
+import {map} from 'rxjs';
 
 import {AppFacade} from '../../state/app.facade';
 import {Plots} from '../../state/app.state';
@@ -26,12 +26,12 @@ import {ParameterSummaryComponent} from '../parameter-summary/parameter-summary.
  */
 interface PlotInfo {
   /** Main plot data */
-  plotData$: Observable<PlotlyPlot>;
+  plotData: Signal<PlotlyPlot>;
   /** Expansion panel title */
   title: string;
 
   /** % diff plot data */
-  diffPlotData$?: Observable<PlotlyPlot>;
+  diffPlotData?: Signal<PlotlyPlot>;
 }
 
 /**
@@ -58,34 +58,34 @@ interface PlotInfo {
   templateUrl: './plots.component.html',
 })
 export class PlotsComponent {
-  /** Hazard components plot data */
-  private hazardComponentsPlotData$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD_COMPONENTS)?.plotData)
+  /** Hazard plot data */
+  private hazardPlotData = computed(
+    () => this.facade.plots().get(Plots.HAZARD).plotData
   );
 
-  /** Hazard % diff plot data */
-  private hazardDiffPlotData$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD_DIFFERENCES)?.plotData)
+  /** Hazard components plot data */
+  private hazardComponentsPlotData = computed(
+    () => this.facade.plots().get(Plots.HAZARD_COMPONENTS).plotData
   );
 
-  /** Hazard plot data */
-  private hazardPlotData$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD)?.plotData)
+  /** Hazard % diff plot data */
+  private hazardDiffPlotData = computed(
+    () => this.facade.plots().get(Plots.HAZARD_DIFFERENCES).plotData
   );
 
   /** Spectra components plot data */
-  private spectraComponentsPlotData$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.SPECTRUM_COMPONENTS)?.plotData)
+  private spectraComponentsPlotData = computed(
+    () => this.facade.plots().get(Plots.SPECTRUM_COMPONENTS).plotData
   );
 
   /** Spectra % diff plot data */
-  private spectraDiffPlotData$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.SPECTRUM_DIFFERENCES)?.plotData)
+  private spectraDiffPlotData = computed(
+    () => this.facade.plots().get(Plots.SPECTRUM_DIFFERENCES).plotData
   );
 
   /** Spectra plot data */
-  private spectraPlotData$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.SPECTRUM)?.plotData)
+  private spectraPlotData = computed(
+    () => this.facade.plots().get(Plots.SPECTRUM).plotData
   );
 
   /** Wheather both the control and settings panels are closed */
@@ -109,29 +109,27 @@ export class PlotsComponent {
   /** The plots */
   plots: PlotInfo[] = [
     {
-      diffPlotData$: this.hazardDiffPlotData$,
-      plotData$: this.hazardPlotData$,
+      diffPlotData: this.hazardDiffPlotData,
+      plotData: this.hazardPlotData,
       title: 'Hazard Curves',
     },
     {
-      diffPlotData$: this.spectraDiffPlotData$,
-      plotData$: this.spectraPlotData$,
+      diffPlotData: this.spectraDiffPlotData,
+      plotData: this.spectraPlotData,
       title: 'Spectra',
     },
     {
-      plotData$: this.hazardComponentsPlotData$,
+      plotData: this.hazardComponentsPlotData,
       title: 'Hazard Components',
     },
     {
-      plotData$: this.spectraComponentsPlotData$,
+      plotData: this.spectraComponentsPlotData,
       title: 'Spectra Components',
     },
   ];
 
   /** Repo metadata */
-  repositories$ = this.facade.usage$.pipe(
-    map(usage => usage?.metadata.repositories)
-  );
+  repositories = computed(() => this.facade.usage()?.metadata.repositories);
 
   constructor(
     private facade: AppFacade,
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.html
index dad4df7af0054e45762c5aac955114a5f4dbfadd..4926ad259f303a5802d370cf6228d361e9e4f444 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.html
@@ -1,20 +1,20 @@
 <div class="grid-container-widescreen-xl margin-y-3">
   <mat-accordion multi>
     <app-table-data-panel
-      [filename]="spectraFilename$ | async"
-      [tableData]="spectraTotalTableData$ | async"
+      [filename]="spectraFilename"
+      [tableData]="spectraTotalTableData()"
       panelTitle="Response Spectra: Total"
     />
 
     <app-table-data-panel
-      [filename]="spectraDiffFilename$ | async"
-      [tableData]="spectraDiffTableData$ | async"
+      [filename]="spectraDiffFilename"
+      [tableData]="spectraDiffTableData()"
       panelTitle="Response Spectra: Percent Difference"
     />
 
     <app-table-data-panel
-      [filename]="spectraComponentsFilename$ | async"
-      [tableData]="spectraComponentsTableData$ | async"
+      [filename]="spectraComponentsFilename"
+      [tableData]="spectraComponentsTableData()"
       panelTitle="Response Spectra: Components"
     />
   </mat-accordion>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.ts
index c9192d6e93940b686dccf4468cd163b65989483e..44e977fed5eca498e092602d9307338b1bfd19e4 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.ts
@@ -1,14 +1,13 @@
 import {AsyncPipe} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed, Signal} from '@angular/core';
 import {MatAccordion} from '@angular/material/expansion';
-import {TableData} from '@ghsc/nshmp-lib-ng/nshmp';
+import {TableData} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
 import {
   SourceType,
   sourceTypeToCapitalCase,
 } from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/model';
 import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
 import equal from 'fast-deep-equal';
-import {combineLatest, map, Observable} from 'rxjs';
 
 import {AppFacade} from '../../state/app.facade';
 import {
@@ -62,98 +61,76 @@ interface ToTableDataOptions {
 })
 export class SpectraDataComponent {
   /** Model values */
-  private models$ = this.facade.controlPanelForm$.pipe(
-    map(form => `${form.value.model}-${form.value.modelCompare}`)
-  );
+  private models = `${this.facade.formGroup.getRawValue().model}-${this.facade.formGroup.getRawValue().modelCompare}`;
 
   /** Filename for hazard components export CSV */
-  spectraComponentsFilename$ = this.models$.pipe(
-    map(models => `spectra-components-${models}.csv`)
-  );
+  spectraComponentsFilename = `spectra-components-${this.models}.csv`;
 
   /** Filename for hazard difference export CSV */
-  spectraDiffFilename$ = this.models$.pipe(
-    map(models => `spectra-%-diff-${models}.csv`)
-  );
+  spectraDiffFilename = `spectra-%-diff-${this.models}.csv`;
 
   /** Filename for hazard export CSV */
-  spectraFilename$ = this.models$.pipe(
-    map(models => `spectra-compare-${models}.csv`)
-  );
+  spectraFilename = `spectra-compare-${this.models}.csv`;
 
   /** Table data for response spectra components */
-  spectraComponentsTableData$ = combineLatest([
-    this.facade.serviceResponses$,
-    this.facade.controlPanelForm$,
-    this.facade.availableModels$,
-  ]).pipe(
-    map(([serviceResponses, form, availableModels]) => {
-      const imts = this.spectraImtRow(serviceResponses);
-
-      const spectraFilter = (spectra: Spectra) =>
-        spectra.sourceType !== SourceType.TOTAL;
-
-      return [
-        imts,
-        ...this.serviceResponsesToTableData({
-          availableModels,
-          name: (modelInfo: Parameter, spectra: Spectra) =>
-            `${modelInfo.display} - ${sourceTypeToCapitalCase(
-              spectra.sourceType
-            )}`,
-          returnPeriod: form.value.returnPeriod,
-          serviceResponses,
-          spectraFilter,
-        }),
-      ];
-    })
-  );
+  spectraComponentsTableData = computed(() => {
+    const imts = this.spectraImtRow(this.facade.serviceResponses());
+
+    const spectraFilter = (spectra: Spectra) =>
+      spectra.sourceType !== SourceType.TOTAL;
+
+    return [
+      imts,
+      ...this.serviceResponsesToTableData({
+        availableModels: this.facade.availableModels(),
+        name: (modelInfo: Parameter, spectra: Spectra) =>
+          `${modelInfo.display} - ${sourceTypeToCapitalCase(
+            spectra.sourceType
+          )}`,
+        returnPeriod: this.facade.formGroup.getRawValue().returnPeriod,
+        serviceResponses: this.facade.serviceResponses(),
+        spectraFilter,
+      }),
+    ];
+  });
 
   /** Table data for response spectra difference */
-  spectraDiffTableData$: Observable<TableData[]> = combineLatest([
-    this.facade.serviceResponses$,
-    this.facade.controlPanelForm$,
-  ]).pipe(
-    map(([serviceResponses, form]) => {
-      const spectraDiff = spectraPercentDifference(
-        serviceResponses,
-        form.value.returnPeriod
-      );
-
-      return [
-        this.spectraImtRow(serviceResponses),
-        {
-          td: spectraDiff.percentDifference,
-          th: 'Percent Difference',
-        },
-      ];
-    })
-  );
+  spectraDiffTableData: Signal<TableData[]> = computed(() => {
+    const serviceResponses = this.facade.serviceResponses();
+
+    const spectraDiff = spectraPercentDifference(
+      serviceResponses,
+      this.facade.formGroup.getRawValue().returnPeriod
+    );
+
+    return [
+      this.spectraImtRow(serviceResponses),
+      {
+        td: spectraDiff.percentDifference,
+        th: 'Percent Difference',
+      },
+    ];
+  });
 
   /** Table data for response spectra */
-  spectraTotalTableData$ = combineLatest([
-    this.facade.serviceResponses$,
-    this.facade.controlPanelForm$,
-    this.facade.availableModels$,
-  ]).pipe(
-    map(([serviceResponses, form, availableModels]) => {
-      const imts = this.spectraImtRow(serviceResponses);
-
-      const spectraFilter = (spectra: Spectra) =>
-        spectra.sourceType === SourceType.TOTAL;
-
-      return [
-        imts,
-        ...this.serviceResponsesToTableData({
-          availableModels,
-          name: (modelInfo: Parameter) => modelInfo.display,
-          returnPeriod: form.value.returnPeriod,
-          serviceResponses,
-          spectraFilter,
-        }),
-      ];
-    })
-  );
+  spectraTotalTableData = computed(() => {
+    const serviceResponses = this.facade.serviceResponses();
+    const imts = this.spectraImtRow(serviceResponses);
+
+    const spectraFilter = (spectra: Spectra) =>
+      spectra.sourceType === SourceType.TOTAL;
+
+    return [
+      imts,
+      ...this.serviceResponsesToTableData({
+        availableModels: this.facade.availableModels(),
+        name: (modelInfo: Parameter) => modelInfo.display,
+        returnPeriod: this.facade.formGroup.getRawValue().returnPeriod,
+        serviceResponses,
+        spectraFilter,
+      }),
+    ];
+  });
 
   constructor(private facade: AppFacade) {}
 
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.html
index b0b49ca7b35221d3a701444f50dcc4ce4e9a6846..d83e9cb1bac3cec10df3c384b1a32e174e51bc8f 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.html
@@ -3,11 +3,11 @@
     <mat-panel-title>{{ panelTitle }}</mat-panel-title>
   </mat-expansion-panel-header>
 
-  <nshmp-lib-ng-export-data-table
+  <nshmp-lib-no-ngrx-export-data-table
     [table]="tableData"
     [filename]="filename | lowercase"
     [buttonText]="buttonText"
   />
 
-  <nshmp-lib-ng-data-table [table]="tableData" />
+  <nshmp-lib-no-ngrx-data-table [table]="tableData" />
 </mat-expansion-panel>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.ts
index 5a61dc77338d6e12ed34196a9a063ca33c9dd63e..43861892039a1f488c4c277018d0eb3a59611240 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.ts
@@ -9,7 +9,7 @@ import {
   NshmpLibNgDataTableComponent,
   NshmpLibNgExportDataTableComponent,
   TableData,
-} from '@ghsc/nshmp-lib-ng/nshmp';
+} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
 
 /**
  * Expansion panel with data table and export.
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.facade.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.facade.ts
index 74e1ff942692b5d4c774ede23a40a78f4142f28e..df6176c68e6a986fad71b27dbd6c52e960e50b83 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.facade.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.facade.ts
@@ -1,18 +1,61 @@
-import {Injectable} from '@angular/core';
-import {ServiceCallInfo} from '@ghsc/nshmp-lib-ng/nshmp';
-import {NshmpPlot} from '@ghsc/nshmp-lib-ng/plot';
-import {HazardUsageResponse} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/hazard-service';
+import {computed, Injectable, Signal, signal} from '@angular/core';
+import {AbstractControl, FormBuilder, Validators} from '@angular/forms';
+import {ActivatedRoute, Router} from '@angular/router';
+import {HazardService, hazardUtils} from '@ghsc/nshmp-lib-no-ngrx/hazard';
+import {
+  NshmpService,
+  ServiceCallInfo,
+  SpinnerService,
+} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmpPlot} from '@ghsc/nshmp-lib-no-ngrx/plot';
+import {
+  HazardCalcResponse,
+  HazardRequestMetadata,
+  HazardResponse,
+  HazardUsageResponse,
+} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/hazard-service';
 import {NshmMetadata} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/nshm-service';
 import {Location} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/geo';
+import {Imt, NehrpSiteClass} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
+import {sourceTypeFromPascalCase} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/model';
+import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
 import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
-import {select, Store} from '@ngrx/store';
-import {FormGroupState} from 'ngrx-forms';
 import {environment} from 'projects/nshmp-apps/src/environments/environment';
-import {Observable} from 'rxjs';
+import {devApps} from 'projects/nshmp-apps/src/shared/utils/applications.utils';
+import {
+  redrawPlots,
+  resetPlotSettings,
+  validateNan,
+} from 'projects/nshmp-apps/src/shared/utils/facade.utils';
+import {catchError, forkJoin, Observable} from 'rxjs';
 
-import {appActions} from './app.actions';
-import {dynamicCompareAppFeature} from './app.reducer';
-import {ControlForm, ServiceResponses} from './app.state';
+import {defaultPlots} from '../utils/app.utils';
+import {createPlots} from '../utils/response-handler.utils';
+import * as responseHandler from '../utils/response-handler.utils';
+import {
+  AppState,
+  ControlForm,
+  controlPanelFormInitialState,
+  initialState,
+  ServiceResponses,
+  Spectra,
+} from './app.state';
+
+/**
+ * URL query.
+ */
+interface Query {
+  imt: string;
+  latitude: string;
+  longitude: string;
+  maxDirection: string;
+  model: string;
+  modelCompare: string;
+  returnPeriod: string;
+  siteClass: string;
+  truncate: string;
+  vs30: string;
+}
 
 /**
  * Entrypoint to NGRX store for dynamic hazard compare application.
@@ -21,128 +64,255 @@ import {ControlForm, ServiceResponses} from './app.state';
   providedIn: 'root',
 })
 export class AppFacade {
+  readonly state = signal<AppState>(initialState());
+
+  readonly formGroup = this.formBuilder.group<ControlForm>(
+    controlPanelFormInitialState()
+  );
+
   /** nshmp-haz-ws web config */
   nshmpHazWs = environment.webServices.nshmpHazWs;
   /** Hazard endpoint */
   serviceEndpoint = this.nshmpHazWs.services.curveServices.hazard;
 
-  constructor(private store: Store) {}
+  constructor(
+    private formBuilder: FormBuilder,
+    private spinnerService: SpinnerService,
+    private nshmpService: NshmpService,
+    private hazardService: HazardService,
+    private route: ActivatedRoute,
+    private router: Router
+  ) {
+    this.addValidators();
+    this.formGroup.valueChanges.subscribe(() => this.updateUrl());
+  }
 
   /**
    * Returns the available dynamic models.
    */
-  get availableModels$(): Observable<Parameter[]> {
-    return this.store.pipe(
-      select(dynamicCompareAppFeature.selectAvailableModels)
-    );
+  get availableModels(): Signal<Parameter[]> {
+    return computed(() => this.state().availableModels);
   }
 
   /**
    * Returns the models that can been compared with selected model.
    */
-  get comparableModels$(): Observable<Parameter[]> {
-    return this.store.pipe(
-      select(dynamicCompareAppFeature.selectComparableModels)
-    );
-  }
-
-  /**
-   * Returns the control panel form state.
-   */
-  get controlPanelForm$(): Observable<FormGroupState<ControlForm>> {
-    return this.store.pipe(
-      select(dynamicCompareAppFeature.selectControlPanelForm)
-    );
+  get comparableModels(): Signal<Parameter[]> {
+    return computed(() => this.state().comparableModels);
   }
 
   /**
    * Returns the `Map` of the plots.
    */
-  get plots$(): Observable<Map<string, NshmpPlot>> {
-    return this.store.pipe(select(dynamicCompareAppFeature.selectPlots));
+  get plots(): Signal<Map<string, NshmpPlot>> {
+    return computed(() => this.state().plots);
   }
 
   /**
    * Returns the metadata of the NSHM observable.
    */
-  get nshmService$(): Observable<NshmMetadata> {
-    return this.store.pipe(select(dynamicCompareAppFeature.selectNshmService));
+  get nshmService(): Signal<NshmMetadata> {
+    return computed(() =>
+      this.state().nshmServices.find(
+        nshmService => nshmService.model === this.formGroup.getRawValue().model
+      )
+    );
   }
 
   /**
    * Returns the service call info.
    */
-  get serviceCallInfo$(): Observable<ServiceCallInfo> {
-    return this.store.pipe(
-      select(dynamicCompareAppFeature.selectServiceCallInfo)
-    );
+  get serviceCallInfo(): Signal<ServiceCallInfo> {
+    return computed(() => this.state().serviceCallInfo);
   }
 
   /**
    * Returns the service responses.
    */
-  get serviceResponses$(): Observable<ServiceResponses> {
-    return this.store.pipe(
-      select(dynamicCompareAppFeature.selectServiceResponses)
-    );
+  get serviceResponses(): Signal<ServiceResponses> {
+    return computed(() => this.state().serviceResponses);
   }
 
   /**
    * Return the usage for the selected model.
    */
-  get usage$(): Observable<HazardUsageResponse> {
-    return this.store.pipe(
-      select(dynamicCompareAppFeature.selectCombinedUsage)
-    );
+  get usage(): Signal<HazardUsageResponse> {
+    return computed(() => this.state().combinedUsage);
   }
 
   /**
    * Return the usage for the selected model.
    */
-  get usageModelA$(): Observable<HazardUsageResponse> {
-    return this.store.pipe(select(dynamicCompareAppFeature.selectUsageModelA));
+  get usageModelA(): Signal<HazardUsageResponse> {
+    return computed(() =>
+      this.state().usageResponses?.get(this.formGroup.getRawValue().model)
+    );
   }
 
   /**
    * Return the usage for the selected model.
    */
-  get usageModelB$(): Observable<HazardUsageResponse> {
-    return this.store.pipe(select(dynamicCompareAppFeature.selectUsageModelB));
+  get usageModelB(): Signal<HazardUsageResponse> {
+    return computed(() =>
+      this.state().usageResponses?.get(
+        this.formGroup.getRawValue().modelCompare
+      )
+    );
   }
 
   /**
    * Call the hazard service.
    */
   callServices(): void {
-    this.store.dispatch(appActions.callServices());
+    this.spinnerService.show(SpinnerService.MESSAGE_SERVICE);
+    const form = this.formGroup.getRawValue();
+
+    const modelUrl = this.serviceCallUrl(
+      form.model,
+      form,
+      this.state().nshmServices
+    );
+
+    const modelCompareUrl = this.serviceCallUrl(
+      form.modelCompare,
+      form,
+      this.state().nshmServices
+    );
+
+    const serviceCallInfo: ServiceCallInfo = {
+      ...this.state().serviceCallInfo,
+      serviceCalls: [modelUrl, modelCompareUrl],
+    };
+
+    const calls: Observable<HazardCalcResponse>[] = [
+      modelUrl,
+      modelCompareUrl,
+    ].map(url => this.nshmpService.callService$(url));
+
+    forkJoin(calls)
+      .pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
+      .subscribe(hazardResponses => {
+        if (hazardResponses.length !== 2) {
+          throw new Error('Does not contain two responses');
+        }
+
+        const modelA = hazardResponses[0];
+        const modelB = hazardResponses[1];
+
+        const serviceResponses: ServiceResponses = {
+          modelA: {
+            hazardResponse: modelA,
+            model: form.model,
+            spectra: this.responseSpectra(modelA.response.hazardCurves, form),
+          },
+          modelB: {
+            hazardResponse: modelB,
+            model: form.modelCompare,
+            spectra: this.responseSpectra(modelB.response.hazardCurves, form),
+          },
+        };
+
+        this.updateState({serviceCallInfo, serviceResponses});
+        this.createPlots();
+        this.spinnerService.remove();
+      });
   }
 
   /**
    * Initialize applicaiton.
    */
   init(): void {
-    this.store.dispatch(appActions.init());
+    this.spinnerService.show(SpinnerService.MESSAGE_METADATA);
+
+    this.hazardService
+      .dynamicNshms$<HazardRequestMetadata>(
+        `${this.nshmpHazWs.url}${this.nshmpHazWs.services.nshms}`,
+        this.serviceEndpoint
+      )
+      .pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
+      .subscribe(({models, nshmServices, usageResponses}) => {
+        this.spinnerService.remove();
+        const serviceCallInfo: ServiceCallInfo = {
+          ...this.state().serviceCallInfo,
+          usage: nshmServices.map(
+            service => `${service.url}${this.serviceEndpoint}`
+          ),
+        };
+
+        this.updateState({
+          availableModels: models,
+          nshmServices,
+          serviceCallInfo,
+          usageResponses,
+        });
+
+        this.onModelChange();
+        this.initialFormSet();
+      });
+  }
+
+  /**
+   * Returns the updated state for a model change.
+   *
+   * @param state The current application state
+   */
+  onModelChange(): void {
+    const comparableModels = this.getComparableModels();
+    const defaultComparableModel = [...comparableModels]?.pop()?.value ?? null;
+    const combinedUsage = this.getCombinedUsage(
+      this.state().usageResponses,
+      this.formGroup.getRawValue().model,
+      defaultComparableModel
+    );
+
+    this.formGroup.patchValue({modelCompare: defaultComparableModel as NshmId});
+
+    this.updateState({
+      combinedUsage,
+      comparableModels,
+      plots: initialState().plots,
+      serviceCallInfo: {
+        ...this.state().serviceCallInfo,
+        serviceCalls: [],
+      },
+      serviceResponses: initialState().serviceResponses,
+    });
   }
 
   /**
    * Redraw the plots.
    */
   plotRedraw(): void {
-    this.store.dispatch(appActions.plotRedraw());
+    redrawPlots(this.state().plots);
   }
 
   /**
    * Reset the control panel.
    */
   resetControlPanel(): void {
-    this.store.dispatch(appActions.resetControlPanel());
+    this.formGroup.reset(controlPanelFormInitialState());
+    this.resetState();
+    this.onModelChange();
   }
 
   /**
    * Reset the plot settings.
    */
   resetSettings(): void {
-    this.store.dispatch(appActions.resetSettings());
+    resetPlotSettings({
+      currentPlots: this.state().plots,
+      defaultPlots: defaultPlots(),
+    });
+  }
+
+  resetState(): void {
+    this.updateState({
+      plots: initialState().plots,
+      serviceCallInfo: {
+        ...this.state().serviceCallInfo,
+        serviceCalls: [],
+      },
+    });
   }
 
   /**
@@ -176,6 +346,248 @@ export class AppFacade {
    * @param location The location
    */
   setLocation(location: Location): void {
-    this.store.dispatch(appActions.setLocation({location}));
+    this.formGroup.patchValue({
+      latitude: location.latitude,
+      longitude: location.longitude,
+    });
+  }
+
+  updateState(state: Partial<AppState>): void {
+    this.state.set({
+      ...this.state(),
+      ...state,
+    });
+  }
+
+  createPlots(): void {
+    const plots = createPlots(this.state(), this.formGroup);
+    this.updateState({plots});
+  }
+
+  /**
+   * Returns the IMT to use.
+   *
+   * @param query The query value
+   * @param defaultImt The default IMT
+   */
+  private getImt(query: string, defaultImt: Imt): Imt {
+    return (
+      Object.values(Imt).find(imt => imt.toString() === query) ?? defaultImt
+    );
+  }
+
+  /**
+   * Returns the model to use.
+   *
+   * @param query The query model
+   * @param defaultModel The default model
+   */
+  private getModel(query: string, defaultModel: NshmId): NshmId {
+    return (
+      Object.values(NshmId).find(model => model.toString() === query) ??
+      defaultModel
+    );
+  }
+
+  /**
+   * Returns the site class to use.
+   *
+   * @param query The query value
+   * @param defaultSiteClass The default site class
+   */
+  private getSiteClass(
+    query: string,
+    defaultSiteClass: NehrpSiteClass
+  ): NehrpSiteClass {
+    return (
+      Object.values(NehrpSiteClass).find(
+        siteClass => siteClass.toString() === query
+      ) ?? defaultSiteClass
+    );
+  }
+
+  /**
+   * Create the response spectra for a specific model and for each source type available.
+   *
+   * @param hazardResponses The hazard responses
+   * @param form The control form values
+   */
+  private responseSpectra(
+    hazardResponses: HazardResponse[],
+    form: ControlForm
+  ): Spectra[] {
+    const spectra: Spectra[] = [];
+    const sourceTypes = [...hazardResponses]
+      .shift()
+      .data.map(data => data.component);
+
+    sourceTypes.forEach(sourceType => {
+      form.sourceType = sourceType;
+      spectra.push({
+        responseSpectra: hazardUtils.responseSpectra(hazardResponses, form),
+        sourceType: sourceTypeFromPascalCase(sourceType),
+      });
+    });
+
+    return spectra;
+  }
+
+  /**
+   * Returns the URL to call for a specific model.
+   *
+   * @param model The NSHM to call
+   * @param formValues The control panel form values
+   * @param nshmServices The NSHM service metadata
+   */
+  private serviceCallUrl(
+    model: NshmId,
+    formValues: ControlForm,
+    nshmServices: NshmMetadata[]
+  ): string {
+    const {latitude, longitude, vs30} = formValues;
+    const service = nshmServices.find(service => service.model === model);
+    return `${service.url}${this.serviceEndpoint}/${longitude}/${latitude}/${vs30}`;
+  }
+
+  private initialFormSet(): void {
+    const query = this.route.snapshot.queryParams as Query;
+    const defaultValues = controlPanelFormInitialState();
+
+    const formValues: ControlForm = {
+      commonReturnPeriods: defaultValues.commonReturnPeriods,
+      imt: this.getImt(query.imt, defaultValues.imt),
+      latitude: query.latitude
+        ? Number.parseFloat(query.latitude)
+        : defaultValues.latitude,
+      longitude: query.longitude
+        ? Number.parseFloat(query.longitude)
+        : defaultValues.longitude,
+      maxDirection:
+        query.maxDirection !== undefined
+          ? (JSON.parse(query.maxDirection) as boolean)
+          : defaultValues.maxDirection,
+      model: this.getModel(query.model, defaultValues.model),
+      modelCompare: this.getModel(
+        query.modelCompare,
+        defaultValues.modelCompare
+      ),
+      returnPeriod: query.returnPeriod
+        ? Number.parseInt(query.returnPeriod, 10)
+        : defaultValues.returnPeriod,
+      siteClass: this.getSiteClass(
+        query.siteClass,
+        defaultValues.siteClass as NehrpSiteClass
+      ),
+      sourceType: null,
+      truncate:
+        query.truncate !== undefined
+          ? (JSON.parse(query.truncate) as boolean)
+          : defaultValues.truncate,
+      vs30: query.vs30 ? Number.parseFloat(query.vs30) : defaultValues.vs30,
+    };
+
+    this.formGroup.patchValue(formValues);
+
+    if (this.formGroup.valid) {
+      this.nshmpService.selectPlotControl();
+      this.callServices();
+    } else if (this.formGroup.getRawValue() !== defaultValues) {
+      this.formGroup.markAsDirty();
+    }
+  }
+
+  private updateUrl(): void {
+    const value = this.formGroup.getRawValue();
+
+    const query: Query = {
+      imt: value.imt,
+      latitude: value.latitude?.toString(),
+      longitude: value.longitude?.toString(),
+      maxDirection: String(value.maxDirection),
+      model: value.model,
+      modelCompare: value.modelCompare,
+      returnPeriod: value.returnPeriod?.toString(),
+      siteClass: value.siteClass,
+      truncate: String(value.truncate),
+      vs30: value.vs30?.toString(),
+    };
+
+    this.router
+      .navigate([devApps().hazard.dynamicCompare.routerLink], {
+        queryParams: {
+          ...query,
+        },
+      })
+      .catch((error: Error) => this.nshmpService.throwError$(error));
+  }
+
+  /**
+   * Returns the combined usage from model and comparable model.
+   *
+   * @param usageResponses The usages
+   * @param model The current selected model
+   * @param comparableModel The current comparable model
+   */
+  private getCombinedUsage(
+    usageResponses: Map<string, HazardUsageResponse>,
+    model: NshmId,
+    comparableModel: string | null
+  ): HazardUsageResponse {
+    let combinedUsage = usageResponses.get(model);
+
+    if (comparableModel !== null) {
+      combinedUsage = responseHandler.combineUsages([
+        usageResponses.get(model),
+        usageResponses.get(comparableModel),
+      ]);
+    }
+
+    return combinedUsage;
+  }
+
+  /**
+   * Returns the available models that are comparable to the select model.
+   *
+   * @param state The application state
+   */
+  private getComparableModels(): Parameter[] {
+    const formValues = this.formGroup.getRawValue();
+    const state = this.state();
+
+    const currentNshmService = state.nshmServices.find(
+      nshmService => nshmService.model === formValues.model
+    );
+
+    const comparableServices = state.nshmServices.filter(
+      nshmService =>
+        nshmService.model !== currentNshmService.model &&
+        nshmService.project === currentNshmService.project
+    );
+
+    return state.availableModels.filter(model =>
+      comparableServices.find(
+        service => service.model.toString() === model.value
+      )
+    );
+  }
+
+  private addValidators(): void {
+    const controls = this.formGroup.controls;
+
+    this.addRequiredValidator(controls.imt);
+    this.addRequiredValidator(controls.imt);
+    this.addRequiredValidator(controls.latitude);
+    this.addRequiredValidator(controls.longitude);
+    this.addRequiredValidator(controls.model);
+    this.addRequiredValidator(controls.modelCompare);
+    this.addRequiredValidator(controls.vs30);
+    this.addRequiredValidator(controls.returnPeriod);
+
+    controls.latitude.addValidators(validateNan());
+    controls.longitude.addValidators(validateNan());
+  }
+
+  private addRequiredValidator(control: AbstractControl): void {
+    control.addValidators(control => Validators.required(control));
   }
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.state.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.state.ts
index e493ca762039d97dc9ff076bb1d88da07bb94734..f96ab64ad0aa26c97fd95b3675579fc4670128ce 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.state.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.state.ts
@@ -1,9 +1,10 @@
 import {
   DynamicHazardControlForm,
-  hazardUtils,
   ResponseSpectra,
 } from '@ghsc/nshmp-lib-ng/hazard';
-import {ServiceCallInfo} from '@ghsc/nshmp-lib-ng/nshmp';
+import {hazardUtils} from '@ghsc/nshmp-lib-no-ngrx/hazard';
+import {ServiceCallInfo} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmpPlot} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {
   HazardCalcResponse,
   HazardUsageResponse,
@@ -16,8 +17,6 @@ import {
 } from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/model';
 import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
 import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
-import {createFormGroupState, FormGroupState, FormState} from 'ngrx-forms';
-import {SharedAppState} from 'projects/nshmp-apps/src/shared/state/shared';
 
 import {defaultPlots} from '../utils/app.utils';
 
@@ -39,17 +38,17 @@ export enum Plots {
 /**
  * Development dynamic hazard compare NGRX state.
  */
-export interface AppState extends SharedAppState {
+export interface AppState {
   /** Available NSHMs */
   availableModels: Parameter[];
   /** Common usage for selected models */
   combinedUsage: HazardUsageResponse;
   /** Models that can compare with a selected model */
   comparableModels: Parameter[];
-  /** Control panel form field state */
-  controlPanelForm: FormState<ControlForm>;
   /** NSHM service metadata */
   nshmServices: NshmMetadata[];
+  /** The application's plots */
+  plots: Map<string, NshmpPlot>;
   /** Service call info */
   serviceCallInfo: ServiceCallInfo;
   /** Hazard service responses */
@@ -108,13 +107,12 @@ export function initialState(): AppState {
     availableModels: [],
     combinedUsage: null,
     comparableModels: [],
-    controlPanelForm: controlPanelFormInitialState(),
     nshmServices: [],
     plots: defaultPlots(),
     serviceCallInfo: {
       serviceCalls: [],
       serviceName: 'Dynamic Hazard Curves',
-      usage: null,
+      usage: [],
     },
     serviceResponses: {
       modelA: null,
@@ -127,12 +125,12 @@ export function initialState(): AppState {
 /**
  * Initial state for the control panel,
  */
-export function controlPanelFormInitialState(): FormGroupState<ControlForm> {
-  return createFormGroupState<ControlForm>(FORM_ID, {
+export function controlPanelFormInitialState(): ControlForm {
+  return {
     ...hazardUtils.hazardDefaultFormValues(),
     imt: Imt.PGA,
     modelCompare: null,
     sourceType: sourceTypeToCapitalCase(SourceType.TOTAL),
     vs30: 760,
-  });
+  };
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/app.utils.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/app.utils.ts
index e7f6b20a24512f9b0c6ddeeb07d30ed975d5b78b..fe276caedd5f5dd0c1de77f547c808d4c85e290f 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/app.utils.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/app.utils.ts
@@ -1,38 +1,22 @@
-import {hazardUtils} from '@ghsc/nshmp-lib-ng/hazard';
-import {
-  NshmpPlot,
-  NshmpPlotSettings,
-  PlotOptions,
-  plotUtils,
-} from '@ghsc/nshmp-lib-ng/plot';
+import {hazardUtils} from '@ghsc/nshmp-lib-no-ngrx/hazard';
+import {NshmpPlot, PlotOptions, plotUtils} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
 import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
-import {createFormGroupState} from 'ngrx-forms';
 
 import {Plots} from '../state/app.state';
 
-/**
- * Plot ngrx-form ids.
- */
-export enum PlotSettingsId {
-  HAZARD = 'ngrx-forms [Hazard Curves Settings]',
-  HAZARD_COMPONENTS = 'ngrx-forms [Hazard Component Curves Settings]',
-  HAZARD_DIFFERENCES = 'ngrx-forms [Hazard Difference Settings]',
-  SPECTRA = 'ngrx-forms [Spectra Settings]',
-  SPECTRA_COMPONENTS = 'ngrx-forms [Spectra Components Settings]',
-  SPECTRA_DIFFERENCES = 'ngrx-forms [Spectra Difference Settings]',
-}
-
 const differenceAspectRatio = '3:1';
 const hazardXLabel = 'Ground Motion (g)';
 const hazardYLabel = 'Annual Frequency of Exceedence';
 const spectraXLabel = 'Spectral Period (s)';
 const spectraYLabel = hazardXLabel;
+
 const plotOptions: PlotOptions = {
   layout: {
     aspectRatio: hazardUtils.HAZARD_ASPECT_RATIO,
   },
 };
+
 const diffPlotOptions: PlotOptions = {
   layout: {
     aspectRatio: differenceAspectRatio,
@@ -113,13 +97,10 @@ function setHazardPlots(plots: Map<string, NshmpPlot>): void {
   plots.set(Plots.HAZARD, {
     label: 'Hazard Curves',
     plotData: hazardPlotData,
-    settingsForm: createFormGroupState<NshmpPlotSettings>(
-      PlotSettingsId.HAZARD,
-      {
-        config: hazardPlotData.config,
-        layout: plotUtils.plotlyLayoutToSettings(hazardPlotData.layout),
-      }
-    ),
+    settingsForm: plotUtils.plotSettingsToFormGroup({
+      config: hazardPlotData.config,
+      layout: plotUtils.plotlyLayoutToSettings(hazardPlotData.layout),
+    }),
   });
 
   const hazardDiffPlotData = plotUtils.defaultPlot({
@@ -134,13 +115,10 @@ function setHazardPlots(plots: Map<string, NshmpPlot>): void {
   plots.set(Plots.HAZARD_DIFFERENCES, {
     label: 'Hazard Curve Differences',
     plotData: hazardDiffPlotData,
-    settingsForm: createFormGroupState<NshmpPlotSettings>(
-      PlotSettingsId.HAZARD_DIFFERENCES,
-      {
-        config: hazardDiffPlotData.config,
-        layout: plotUtils.plotlyLayoutToSettings(hazardDiffPlotData.layout),
-      }
-    ),
+    settingsForm: plotUtils.plotSettingsToFormGroup({
+      config: hazardDiffPlotData.config,
+      layout: plotUtils.plotlyLayoutToSettings(hazardDiffPlotData.layout),
+    }),
   });
 
   const hazardComponentsPlotData = plotUtils.defaultPlot({
@@ -155,15 +133,10 @@ function setHazardPlots(plots: Map<string, NshmpPlot>): void {
   plots.set(Plots.HAZARD_COMPONENTS, {
     label: 'Hazard Component Curves',
     plotData: hazardComponentsPlotData,
-    settingsForm: createFormGroupState<NshmpPlotSettings>(
-      PlotSettingsId.HAZARD_COMPONENTS,
-      {
-        config: hazardComponentsPlotData.config,
-        layout: plotUtils.plotlyLayoutToSettings(
-          hazardComponentsPlotData.layout
-        ),
-      }
-    ),
+    settingsForm: plotUtils.plotSettingsToFormGroup({
+      config: hazardComponentsPlotData.config,
+      layout: plotUtils.plotlyLayoutToSettings(hazardComponentsPlotData.layout),
+    }),
   });
 }
 
@@ -185,13 +158,10 @@ function setSpectraPlots(plots: Map<string, NshmpPlot>): void {
   plots.set(Plots.SPECTRUM, {
     label: 'Spectrum',
     plotData: spectrumPlotData,
-    settingsForm: createFormGroupState<NshmpPlotSettings>(
-      PlotSettingsId.SPECTRA,
-      {
-        config: spectrumPlotData.config,
-        layout: plotUtils.plotlyLayoutToSettings(spectrumPlotData.layout),
-      }
-    ),
+    settingsForm: plotUtils.plotSettingsToFormGroup({
+      config: spectrumPlotData.config,
+      layout: plotUtils.plotlyLayoutToSettings(spectrumPlotData.layout),
+    }),
   });
 
   const spectrumDiffPlotData = plotUtils.defaultPlot({
@@ -206,13 +176,10 @@ function setSpectraPlots(plots: Map<string, NshmpPlot>): void {
   plots.set(Plots.SPECTRUM_DIFFERENCES, {
     label: 'Spectrum Differences',
     plotData: spectrumDiffPlotData,
-    settingsForm: createFormGroupState<NshmpPlotSettings>(
-      PlotSettingsId.SPECTRA_DIFFERENCES,
-      {
-        config: spectrumDiffPlotData.config,
-        layout: plotUtils.plotlyLayoutToSettings(spectrumDiffPlotData.layout),
-      }
-    ),
+    settingsForm: plotUtils.plotSettingsToFormGroup({
+      config: spectrumDiffPlotData.config,
+      layout: plotUtils.plotlyLayoutToSettings(spectrumDiffPlotData.layout),
+    }),
   });
 
   const spectraComponentsPlotData = plotUtils.defaultPlot({
@@ -227,14 +194,11 @@ function setSpectraPlots(plots: Map<string, NshmpPlot>): void {
   plots.set(Plots.SPECTRUM_COMPONENTS, {
     label: 'Spectra Component Curves',
     plotData: spectraComponentsPlotData,
-    settingsForm: createFormGroupState<NshmpPlotSettings>(
-      PlotSettingsId.SPECTRA_COMPONENTS,
-      {
-        config: spectraComponentsPlotData.config,
-        layout: plotUtils.plotlyLayoutToSettings(
-          spectraComponentsPlotData.layout
-        ),
-      }
-    ),
+    settingsForm: plotUtils.plotSettingsToFormGroup({
+      config: spectraComponentsPlotData.config,
+      layout: plotUtils.plotlyLayoutToSettings(
+        spectraComponentsPlotData.layout
+      ),
+    }),
   });
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/hazard-plots.utils.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/hazard-plots.utils.ts
index 1182fbcecb119d2ffc3eeffcfb52f49c72bdb2ab..e480a7c02610a797c626ae30918a27d71c49a522 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/hazard-plots.utils.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/hazard-plots.utils.ts
@@ -1,6 +1,6 @@
-import {hazardUtils} from '@ghsc/nshmp-lib-ng/hazard';
-import {NumberBounds} from '@ghsc/nshmp-lib-ng/nshmp';
-import {NshmpPlot, plotUtils} from '@ghsc/nshmp-lib-ng/plot';
+import {hazardUtils} from '@ghsc/nshmp-lib-no-ngrx/hazard';
+import {FormGroupControls, NumberBounds} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmpPlot, plotUtils} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {
   HazardCurve,
   HazardResponse,
@@ -82,7 +82,10 @@ interface PlotDash {
  *
  * @param state The app state
  */
-export function createHazardPlots(state: AppState): Map<string, NshmpPlot> {
+export function createHazardPlots(
+  state: AppState,
+  formGroup: FormGroupControls<ControlForm>
+): Map<string, NshmpPlot> {
   const plots = new Map<string, NshmpPlot>();
 
   const hazardPlot = state.plots.get(Plots.HAZARD);
@@ -93,7 +96,7 @@ export function createHazardPlots(state: AppState): Map<string, NshmpPlot> {
     state.serviceResponses,
     state.availableModels,
     hazardPlot,
-    state.controlPanelForm.value
+    formGroup.getRawValue()
   );
 
   plots.set(Plots.HAZARD, {
@@ -103,7 +106,7 @@ export function createHazardPlots(state: AppState): Map<string, NshmpPlot> {
 
   const hazardCurveDifference = createHazardDiffPlot(
     state.serviceResponses,
-    state.controlPanelForm.value.imt,
+    formGroup.getRawValue().imt,
     hazardDiffPlot
   );
 
@@ -116,7 +119,7 @@ export function createHazardPlots(state: AppState): Map<string, NshmpPlot> {
     state.serviceResponses,
     state.availableModels,
     hazardComponentsPlot,
-    state.controlPanelForm.value
+    formGroup.getRawValue()
   );
 
   plots.set(Plots.HAZARD_COMPONENTS, {
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/response-handler.utils.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/response-handler.utils.ts
index 5de1076c701ac81a87bf7eee32de7dd0c3f5f92d..c393e7ad8cf154efc1593a5416255b1bb6253cb1 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/response-handler.utils.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/response-handler.utils.ts
@@ -1,8 +1,9 @@
-import {NshmpPlot} from '@ghsc/nshmp-lib-ng/plot';
+import {FormGroupControls} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmpPlot} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {HazardUsageResponse} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/hazard-service';
 import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
 
-import {AppState} from '../state/app.state';
+import {AppState, ControlForm} from '../state/app.state';
 import {createHazardPlots} from './hazard-plots.utils';
 import {createSpectraPlots} from './spectra-plots.utils';
 
@@ -59,7 +60,10 @@ export function combineUsages(
  *
  * @param state The app state
  */
-export function createPlots(state: AppState): Map<string, NshmpPlot> {
+export function createPlots(
+  state: AppState,
+  formGroup: FormGroupControls<ControlForm>
+): Map<string, NshmpPlot> {
   if (
     state.serviceResponses.modelA === null ||
     state.serviceResponses.modelB === null
@@ -67,8 +71,8 @@ export function createPlots(state: AppState): Map<string, NshmpPlot> {
     return state.plots;
   }
 
-  const hazardPlots = createHazardPlots(state);
-  const spectraPlots = createSpectraPlots(state);
+  const hazardPlots = createHazardPlots(state, formGroup);
+  const spectraPlots = createSpectraPlots(state, formGroup);
   return new Map([...hazardPlots.entries(), ...spectraPlots.entries()]);
 }
 
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/spectra-plots.utils.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/spectra-plots.utils.ts
index 7f4d63fbc8de9544240f969a3f742a76a4f821e8..39cdd9962f0bb0cc6622d4beb0a5c141c6e1c5f2 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/spectra-plots.utils.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/spectra-plots.utils.ts
@@ -1,4 +1,5 @@
-import {NshmpPlot} from '@ghsc/nshmp-lib-ng/plot';
+import {FormGroupControls} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmpPlot} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {Imt, imtToPeriod} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
 import {
   SourceType,
@@ -110,7 +111,10 @@ interface PlotColors {
  *
  * @param state The app state
  */
-export function createSpectraPlots(state: AppState): Map<string, NshmpPlot> {
+export function createSpectraPlots(
+  state: AppState,
+  formGroup: FormGroupControls<ControlForm>
+): Map<string, NshmpPlot> {
   const plots = new Map<string, NshmpPlot>();
 
   const spectraPlot = state.plots.get(Plots.SPECTRUM);
@@ -120,7 +124,7 @@ export function createSpectraPlots(state: AppState): Map<string, NshmpPlot> {
   const spectraCurves = createSpectraCurvesPlot(
     state.serviceResponses,
     state.availableModels,
-    state.controlPanelForm.value,
+    formGroup.getRawValue(),
     spectraPlot
   );
 
@@ -132,7 +136,7 @@ export function createSpectraPlots(state: AppState): Map<string, NshmpPlot> {
   const spectraComponents = createSpectraComponentsPlot(
     state.serviceResponses,
     state.availableModels,
-    state.controlPanelForm.value,
+    formGroup.getRawValue(),
     spectraPlot
   );
 
@@ -144,7 +148,7 @@ export function createSpectraPlots(state: AppState): Map<string, NshmpPlot> {
   const spectraDiff = createSpectraDiffPlot(
     state.serviceResponses,
     state.availableModels,
-    state.controlPanelForm.value,
+    formGroup.getRawValue(),
     spectraDiffPlot
   );
 
diff --git a/projects/nshmp-apps/src/environments/environment.ts b/projects/nshmp-apps/src/environments/environment.ts
index 9e5f90d516b5f90965c8a202e809a4e13ff04283..2cf15b89f0705c54d673933432345458f0ad848c 100644
--- a/projects/nshmp-apps/src/environments/environment.ts
+++ b/projects/nshmp-apps/src/environments/environment.ts
@@ -6,5 +6,5 @@ import {webServices} from './web-services';
  */
 export const environment: AngularEnvironment = {
   production: false,
-  webServices: webServices('https://earthquake.usgs.gov'),
+  webServices: webServices('https://staging-earthquake.usgs.gov'),
 };
diff --git a/projects/nshmp-apps/src/shared/models/plot-settings-panel.model.ts b/projects/nshmp-apps/src/shared/models/plot-settings-panel.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..641b80c49384b755340ea4f69414ebc8e3887c23
--- /dev/null
+++ b/projects/nshmp-apps/src/shared/models/plot-settings-panel.model.ts
@@ -0,0 +1,5 @@
+import {PlotSettingsPanel} from '@ghsc/nshmp-lib-no-ngrx/plot';
+
+export interface PlotSettingsPanelId extends PlotSettingsPanel {
+  id: string;
+}
diff --git a/projects/nshmp-apps/src/shared/utils/facade.utils.ts b/projects/nshmp-apps/src/shared/utils/facade.utils.ts
index a1ff3e61b0d01d38538a06ede5c14a3111661626..bb3ba5d49fa434844b96c449aef62e3c39ae0809 100644
--- a/projects/nshmp-apps/src/shared/utils/facade.utils.ts
+++ b/projects/nshmp-apps/src/shared/utils/facade.utils.ts
@@ -1,3 +1,4 @@
+import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms';
 import {NshmpPlot} from '@ghsc/nshmp-lib-no-ngrx/plot';
 
 export interface ResetPlotSettingsProps {
@@ -34,3 +35,11 @@ export function redrawPlots(
 
   return updatedPlots;
 }
+
+export function validateNan(): ValidatorFn {
+  return (
+    control: AbstractControl<number, number>
+  ): ValidationErrors | null => {
+    return isNaN(control.value) ? {nan: {value: control.value}} : null;
+  };
+}