diff --git a/package-lock.json b/package-lock.json index 593966d57584eb4e481515b94be6321f62d65bf9..50f3a4fb0f31c6244117c019a2a970ed5184b143 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@angular/router": "18.1.1", "@asymmetrik/ngx-leaflet": "^18.0.1", "@compodoc/compodoc": "^1.1.25", - "@ghsc/disagg-d3": "^0.9.0", + "@ghsc/disagg-d3": "^0.12.0", "@ghsc/nshmp-lib-ng": "^18.15.0", "@ghsc/nshmp-template": "^18.0.3", "@ghsc/nshmp-utils-ts": "^3.8.1", @@ -4272,9 +4272,9 @@ "license": "MIT" }, "node_modules/@ghsc/disagg-d3": { - "version": "0.9.0", - "resolved": "https://code.usgs.gov/api/v4/projects/4335/packages/npm/@ghsc/disagg-d3/-/@ghsc/disagg-d3-0.9.0.tgz", - "integrity": "sha1-FFrDhBgysXUcubuws7o7yK1WrCQ=", + "version": "0.12.0", + "resolved": "https://code.usgs.gov/api/v4/projects/4335/packages/npm/@ghsc/disagg-d3/-/@ghsc/disagg-d3-0.12.0.tgz", + "integrity": "sha1-1CgSNTwwouZmtGiJnHTBbOd354k=", "dependencies": { "@ghsc/nshmp-utils-ts": "^3.0.0", "d3": "^7.6.1" diff --git a/package.json b/package.json index 43b7c9b6c141fda6982e3e4501ede4ca56594544..8141a044bc2d724ed1758d2f996d243fb2aa010f 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@angular/router": "18.1.1", "@asymmetrik/ngx-leaflet": "^18.0.1", "@compodoc/compodoc": "^1.1.25", - "@ghsc/disagg-d3": "^0.9.0", + "@ghsc/disagg-d3": "^0.12.0", "@ghsc/nshmp-lib-ng": "^18.15.0", "@ghsc/nshmp-template": "^18.0.3", "@ghsc/nshmp-utils-ts": "^3.8.1", diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/content/content.component.html b/projects/nshmp-apps/src/app/hazard/disagg/components/content/content.component.html index 2c11841d1df9540ce059156a505c372e6152a6cc..58c70ef6fbce0aad9d77bf718c3f346c40a6fc92 100644 --- a/projects/nshmp-apps/src/app/hazard/disagg/components/content/content.component.html +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/content/content.component.html @@ -1,4 +1,4 @@ -<div class="height-full overflow-auto"> +<div class="height-full overflow-auto report"> <div class="grid-container-widescreen"> <!-- Report title, show only on print --> <div class="print-content-only"> @@ -42,32 +42,54 @@ <!-- Summary report --> <mat-expansion-panel - class="summary-report print-page-break" + class="summary-report print-full-page" [expanded]="disaggData()" [disabled]="disaggData() === null" > <mat-expansion-panel-header> - <mat-panel-title>Disaggregation Summary</mat-panel-title> + <mat-panel-title + >Disaggregation Summary: + {{ formGroup.getRawValue().disaggComponent }}</mat-panel-title + > </mat-expansion-panel-header> - <app-disagg-summary /> + <app-disagg-summary [componentData]="componentData()" /> </mat-expansion-panel> <!-- Contributions --> <mat-expansion-panel - class="contributions print-page-break" - [expanded]="componentData() !== null" - [disabled]="componentData() === null" + class="contributions print-full-page" + [expanded]="componentData()?.sources.length > 0" + [disabled]="componentData()?.sources.length === 0" > <mat-expansion-panel-header> - <mat-panel-title>Disaggregation Contributions</mat-panel-title> + <mat-panel-title + >Disaggregation Contributions: + {{ formGroup.getRawValue().disaggComponent }}</mat-panel-title + > </mat-expansion-panel-header> - <app-disagg-contributors /> + <app-disagg-contributors [componentData]="componentData()" /> + </mat-expansion-panel> + + <!-- Data --> + <mat-expansion-panel + class="print-display-none" + [expanded]="componentData()?.data.length > 0" + [disabled]="componentData()?.data.length === 0" + > + <mat-expansion-panel-header> + <mat-panel-title + >Disaggregation Data: + {{ formGroup.getRawValue().disaggComponent }}</mat-panel-title + > + </mat-expansion-panel-header> + + <app-disagg-data [componentData]="componentData()" /> </mat-expansion-panel> <!-- App metadata --> - <mat-expansion-panel expanded class="print-page-break"> + <mat-expansion-panel class="print-page-break print-full-page" expanded> <mat-expansion-panel-header> <mat-panel-title>Application Metadata</mat-panel-title> </mat-expansion-panel-header> diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/content/content.component.scss b/projects/nshmp-apps/src/app/hazard/disagg/components/content/content.component.scss index 3094eb336b61d44fa8892d78b1b5160d064110e3..50361509a44fc2fdc39cf9b3d1e9aae8aa45c5e2 100644 --- a/projects/nshmp-apps/src/app/hazard/disagg/components/content/content.component.scss +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/content/content.component.scss @@ -1,6 +1,7 @@ @media print { mat-accordion { mat-expansion-panel { + overflow: visible; box-shadow: none !important; } } diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/content/content.component.ts b/projects/nshmp-apps/src/app/hazard/disagg/components/content/content.component.ts index 1e50c4085538f6241a74522fde885abd5fe23ec2..c7929d96387e4700b7df35b7f62765f98ba085dd 100644 --- a/projects/nshmp-apps/src/app/hazard/disagg/components/content/content.component.ts +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/content/content.component.ts @@ -1,15 +1,12 @@ +import {AsyncPipe} from '@angular/common'; import {Component, computed} from '@angular/core'; import {MatDivider} from '@angular/material/divider'; -import { - MatAccordion, - MatExpansionPanel, - MatExpansionPanelHeader, - MatExpansionPanelTitle, -} from '@angular/material/expansion'; +import {MatExpansionModule} from '@angular/material/expansion'; import {NshmpLibNgAppMetadataComponent} from '@ghsc/nshmp-lib-ng/nshmp'; import {AppService} from '../../services/app.service'; import {DisaggContributorsComponent} from '../disagg-contributors/disagg-contributors.component'; +import {DisaggDataComponent} from '../disagg-data/disagg-data.component'; import {DisaggSummaryComponent} from '../disagg-summary/disagg-summary.component'; import {GeoDisaggComponent} from '../geo-disagg/geo-disagg.component'; import {ParameterSummaryComponent} from '../parameter-summary/parameter-summary.component'; @@ -24,17 +21,16 @@ import {PlotsComponent} from '../plots/plots.component'; */ @Component({ imports: [ - MatAccordion, - MatExpansionPanel, - MatExpansionPanelHeader, - MatExpansionPanelTitle, + MatExpansionModule, PlotsComponent, MatDivider, GeoDisaggComponent, ParameterSummaryComponent, + NshmpLibNgAppMetadataComponent, DisaggSummaryComponent, DisaggContributorsComponent, - NshmpLibNgAppMetadataComponent, + DisaggDataComponent, + AsyncPipe, ], selector: 'app-content', standalone: true, @@ -42,6 +38,13 @@ import {PlotsComponent} from '../plots/plots.component'; templateUrl: './content.component.html', }) export class ContentComponent { + componentData = this.service.componentData; + /** Disaggregation data */ + disaggData = this.service.disaggData; + formGroup = this.service.formGroup; + + hasData = computed(() => this.service.serviceResponse() !== null); + /** Has geo disagg layers */ hasLayers = computed(() => { const serviceResponse = this.service.serviceResponse(); @@ -61,12 +64,6 @@ export class ContentComponent { return layers !== undefined; }); - /** Disaggregation component data */ - componentData = this.service.componentData; - - /** Disaggregation data */ - disaggData = this.service.disaggData; - /** Repo metadata */ repositories = computed(() => this.service.usage()?.metadata.repositories); diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-contributors/disagg-contributors.component.html b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-contributors/disagg-contributors.component.html index 70779f8ec4b8d2c87d22fed5d6366d1c4b30a1e8..e13aeab01da343b42d1456b2908aff9f367943ce 100644 --- a/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-contributors/disagg-contributors.component.html +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-contributors/disagg-contributors.component.html @@ -1,70 +1,74 @@ <!-- Disagg contributions --> -@if (componentData$ | async; as componentData) { - <div class="disagg-contributions"> - @if (serviceResponse()) { - <div class="print-display-none"> - <button - mat-raised-button - color="primary" - (click)=" - service.saveContributions(componentData(), form.getRawValue()) - " - > - Export as CSV - </button> - </div> +@if (componentData) { + @if (componentData?.sources?.length > 0) { + <div class="disagg-contributions"> + @if (serviceResponse()) { + @if (showExportButton) { + <div class="print-display-none"> + <button + mat-raised-button + color="primary" + (click)=" + service.saveContributions(componentData, form.getRawValue()) + " + > + Export as CSV + </button> + </div> - <mat-divider /> + <mat-divider /> + } - <div class="horizontal-scrolling"> - <div> - <table #table class="contributing-sources grid-col-12"> - <thead> - <tr> - <th nowrap> - Source Set - <mat-icon - class="down-arrow" - aria-label="Down arrow icon" - fontIcon="subdirectory_arrow_right" - /> - Source - </th> - <th nowrap>Type</th> - <th nowrap title="Distance (km)">r</th> - <th nowrap title="Magnitude">m</th> - <th nowrap title="Epsilon (mean values)">ε<sub>0</sub></th> - <th nowrap title="Longitude">lon</th> - <th nowrap title="Latitude">lat</th> - <th nowrap title="Azimuth">az</th> - <th nowrap title="Percent contributed">%</th> - </tr> - </thead> - - <tbody> - @for (data of componentData()?.sources; track data) { - <tr [ngClass]="{'contributor-set': data.type === 'SET'}"> - @if (data.type === 'SET') { - <td nowrap>{{ data?.name }}</td> - <td nowrap>{{ data?.source }}</td> - <td colspan="6"></td> - } @else { - <td nowrap class="indent-name">{{ data?.name }}</td> - <td></td> - <td nowrap>{{ data?.r | number: '1.2-2' }}</td> - <td nowrap>{{ data?.m | number: '1.2-2' }}</td> - <td nowrap>{{ dataEpsilon(data) | number: '1.2-2' }}</td> - <td nowrap>{{ data?.longitude | formatLongitude }}</td> - <td nowrap>{{ data?.latitude | formatLatitude }}</td> - <td nowrap>{{ data?.azimuth | number: '1.2-2' }}</td> - } - <td nowrap>{{ data?.contribution }}</td> + <div class="horizontal-scrolling"> + <div> + <table #table class="contributing-sources grid-col-12"> + <thead> + <tr> + <th nowrap> + Source Set + <mat-icon + class="down-arrow" + aria-label="Down arrow icon" + fontIcon="subdirectory_arrow_right" + /> + Source + </th> + <th nowrap>Type</th> + <th nowrap title="Distance (km)">r</th> + <th nowrap title="Magnitude">m</th> + <th nowrap title="Epsilon (mean values)">ε<sub>0</sub></th> + <th nowrap title="Longitude">lon</th> + <th nowrap title="Latitude">lat</th> + <th nowrap title="Azimuth">az</th> + <th nowrap title="Percent contributed">%</th> </tr> - } - </tbody> - </table> + </thead> + + <tbody> + @for (data of componentData?.sources; track data) { + <tr [ngClass]="{'contributor-set': data.type === 'SET'}"> + @if (data.type === 'SET') { + <td nowrap>{{ data?.name }}</td> + <td nowrap>{{ data?.source }}</td> + <td colspan="6"></td> + } @else { + <td nowrap class="indent-name">{{ data?.name }}</td> + <td></td> + <td nowrap>{{ data?.r | number: '1.2-2' }}</td> + <td nowrap>{{ data?.m | number: '1.2-2' }}</td> + <td nowrap>{{ dataEpsilon(data) | number: '1.2-2' }}</td> + <td nowrap>{{ data?.longitude | formatLongitude }}</td> + <td nowrap>{{ data?.latitude | formatLatitude }}</td> + <td nowrap>{{ data?.azimuth | number: '1.2-2' }}</td> + } + <td nowrap>{{ data?.contribution }}</td> + </tr> + } + </tbody> + </table> + </div> </div> - </div> - } - </div> + } + </div> + } } diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-contributors/disagg-contributors.component.ts b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-contributors/disagg-contributors.component.ts index 3b84a7e865e32281bc750954bfc6f2bb75984e5d..e0fa11c3c2350d80fac1b7c8f438a3d3ed90249c 100644 --- a/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-contributors/disagg-contributors.component.ts +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-contributors/disagg-contributors.component.ts @@ -1,5 +1,5 @@ import {AsyncPipe, DecimalPipe, NgClass} from '@angular/common'; -import {Component} from '@angular/core'; +import {Component, Input} from '@angular/core'; import {MatButton} from '@angular/material/button'; import {MatDivider} from '@angular/material/divider'; import {MatIcon} from '@angular/material/icon'; @@ -7,8 +7,10 @@ import { FormatLatitudePipe, FormatLongitudePipe, } from '@ghsc/nshmp-lib-ng/hazard'; -import {DisaggSource} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/disagg-service'; -import {map} from 'rxjs'; +import { + DisaggComponentData, + DisaggSource, +} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/disagg-service'; import {AppService} from '../../services/app.service'; @@ -32,11 +34,12 @@ import {AppService} from '../../services/app.service'; templateUrl: './disagg-contributors.component.html', }) export class DisaggContributorsComponent { - /** Disaggregation data state */ - componentData$ = - this.service.formGroup.controls.disaggComponent.valueChanges.pipe( - map(() => this.service.componentData) - ); + /** Disaggregation component data state */ + @Input({required: true}) + componentData: DisaggComponentData; + + @Input() + showExportButton = true; /** Form field state */ form = this.service.formGroup; diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-data/disagg-data.component.html b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-data/disagg-data.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e4e80db7e5bc96ff66d803bd845ed52d921a145f --- /dev/null +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-data/disagg-data.component.html @@ -0,0 +1,48 @@ +@if (componentData) { + @if (componentData.data.length > 0) { + @if (showExportButton) { + <!-- Export button --> + <div class="print-display-none"> + <button + class="export-button" + mat-raised-button + color="primary" + [disabled]="componentData.data === null" + (click)="service.saveComponentData()" + > + Export Data as CSV + </button> + </div> + + <mat-divider /> + } + + <div class="horizontal-scrolling disagg-data"> + <div> + <table class="grid-col-12"> + <thead> + <tr> + <th nowrap>{{ metadata().rlabel }}</th> + <th nowrap>{{ metadata().mlabel }}</th> + @for (key of epsilonKeys(); track key) { + <th nowrap>{{ key }}</th> + } + </tr> + </thead> + <tbody> + @for (data of componentData.data; track data) { + <tr> + <td noWrap>{{ data.r }}</td> + <td nowrap>{{ data.m }}</td> + + @for (binData of toBinData(data); track $index) { + <td nowrap>{{ binData }}</td> + } + </tr> + } + </tbody> + </table> + </div> + </div> + } +} diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-data/disagg-data.component.scss b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-data/disagg-data.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..f42f51ca2b2b9ab2e7bb925d75787fb5abff4dd7 --- /dev/null +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-data/disagg-data.component.scss @@ -0,0 +1,34 @@ +table { + th:first-child, + td:first-child { + text-align: left; + } + + th, + td { + background: none; + border: none; + text-align: center; + } + + th { + background-color: inherit; + border-bottom: 3px solid #ddd; + font-size: clamp(12px, 8px + 1vw, 16px); + padding: 1em; + } + + td { + font-size: clamp(8px, 4px + 1vw, 14px); + padding: 0 clamp(0.25em, 0.1em + 1vw, 1em); + } + + tr { + line-height: clamp(0.2em, 0.1em + 1.5vw, 1.5em); + } +} + +.disagg-data { + max-height: 1000px; + overflow: scroll; +} diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-data/disagg-data.component.spec.ts b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-data/disagg-data.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5c0d7072f5e243725fd3b5f3547df9ef7874ed9 --- /dev/null +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-data/disagg-data.component.spec.ts @@ -0,0 +1,30 @@ +import {provideHttpClient} from '@angular/common/http'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {provideNoopAnimations} from '@angular/platform-browser/animations'; +import {provideRouter} from '@angular/router'; + +import {DisaggDataComponent} from './disagg-data.component'; + +describe('DisaggDataComponent', () => { + let component: DisaggDataComponent; + let fixture: ComponentFixture<DisaggDataComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DisaggDataComponent], + providers: [ + provideHttpClient(), + provideNoopAnimations(), + provideRouter([]), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DisaggDataComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-data/disagg-data.component.ts b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-data/disagg-data.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..092e0d9a1a4adf9915de235087700e7adb8495e5 --- /dev/null +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-data/disagg-data.component.ts @@ -0,0 +1,50 @@ +import {AsyncPipe} from '@angular/common'; +import {Component, computed, Input} from '@angular/core'; +import {MatButton} from '@angular/material/button'; +import {MatDivider} from '@angular/material/divider'; +import { + DisaggComponentData, + DisaggData, +} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/disagg-service'; + +import {AppService} from '../../services/app.service'; + +@Component({ + imports: [AsyncPipe, MatDivider, MatButton], + selector: 'app-disagg-data', + standalone: true, + styleUrl: './disagg-data.component.scss', + templateUrl: './disagg-data.component.html', +}) +export class DisaggDataComponent { + /** Disaggregation component data state */ + @Input({required: true}) + componentData: DisaggComponentData; + + @Input() + showExportButton = true; + + metadata = computed(() => this.service.serviceResponse().response.metadata); + + constructor(public service: AppService) {} + + toBinData(data: DisaggData): string[] { + const bins = this.service.serviceResponse().response.metadata.εbins; + + const binData = bins.map( + bin => data.εdata.find(data => data.εbin === bin.id)?.value ?? 0 + ); + + const total = binData.reduce((a, b) => a + b, 0); + + return [total, ...binData].map(num => num.toExponential(2)); + } + + epsilonKeys(): string[] { + const keys = this.componentData.summary.find( + data => data.name.toLowerCase() === 'epsilon keys' + ); + + return ['ALL_ε', ...keys.data.map(data => `${data.name}=${data.value}`)]; + } +} diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-summary/disagg-summary.component.html b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-summary/disagg-summary.component.html index 18d9b7a89c2a4ec5917ad7002e1926d78c68889f..2ed8ee819af7907f514fa42f918c0b6f6a5d346a 100644 --- a/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-summary/disagg-summary.component.html +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-summary/disagg-summary.component.html @@ -1,21 +1,23 @@ -@if (componentData$ | async; as componentData) { +@if (componentData) { <div class="disagg-summaries"> <!-- Summaries --> <div class="summary-group"> - <div class="print-display-none"> - <button - mat-raised-button - color="primary" - (click)="service.saveSummary(componentData(), form.getRawValue())" - > - Export as Text - </button> - </div> + @if (showExportButton) { + <div class="print-display-none"> + <button + mat-raised-button + color="primary" + (click)="service.saveSummary(componentData, form.getRawValue())" + > + Export as Text + </button> + </div> - <mat-divider /> + <mat-divider /> + } <div class="grid-row"> - @for (summary of componentData()?.summary; track summary) { + @for (summary of componentData?.summary; track summary) { <div class="grid-col-12 mobile:grid-col-6 desktop-lg:grid-col-4 widescreen:grid-col-3 summary-values print-flex-basis-half" > diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-summary/disagg-summary.component.ts b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-summary/disagg-summary.component.ts index 78a0fb12ae721311ed99d38883a56bce5ecd5f85..95dc424623c5587f06f8e29c285148f28dc90742 100644 --- a/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-summary/disagg-summary.component.ts +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/disagg-summary/disagg-summary.component.ts @@ -1,9 +1,9 @@ import {AsyncPipe} from '@angular/common'; -import {Component} from '@angular/core'; +import {Component, Input} from '@angular/core'; import {MatButton} from '@angular/material/button'; import {MatDivider} from '@angular/material/divider'; import {MatList, MatListItem} from '@angular/material/list'; -import {map} from 'rxjs'; +import {DisaggComponentData} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/disagg-service'; import {AppService} from '../../services/app.service'; @@ -19,10 +19,11 @@ import {AppService} from '../../services/app.service'; }) export class DisaggSummaryComponent { /** Disaggregation component data state */ - componentData$ = - this.service.formGroup.controls.disaggComponent.valueChanges.pipe( - map(() => this.service.componentData) - ); + @Input({required: true}) + componentData: DisaggComponentData; + + @Input() + showExportButton = true; /** Form field state */ form = this.service.formGroup; diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/plots/plots.component.html b/projects/nshmp-apps/src/app/hazard/disagg/components/plots/plots.component.html index e0016b796e15432eab0b5ed16e7ff990860595c8..edd15e955156f04b1603574a53b9597484189c97 100644 --- a/projects/nshmp-apps/src/app/hazard/disagg/components/plots/plots.component.html +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/plots/plots.component.html @@ -7,20 +7,20 @@ [disabled]="disaggData() === null" (click)="exportReport()" > - Print Report + Print Report (PDF) </button> </div> - <!-- Export button --> + <!-- Export summary button --> <div> <button class="export-button" mat-raised-button color="primary" [disabled]="disaggData() === null" - (click)="service.saveComponentData()" + (click)="service.saveSummaryReport()" > - Export Data as CSV + Save Report (TXT) </button> </div> </div> @@ -49,6 +49,7 @@ <!-- Disagg plot --> <nshmp-lib-ng-hazard-disagg-plot + #disaggPlot [inputs]="{ component: form.value.disaggComponent, imt: form.value.imt, diff --git a/projects/nshmp-apps/src/app/hazard/disagg/components/plots/plots.component.ts b/projects/nshmp-apps/src/app/hazard/disagg/components/plots/plots.component.ts index 9e5b92f31000b1232e92a6aa643dabbfffa04a4d..a990f887b96f452bad4adebc02ce4e06fc613f9c 100644 --- a/projects/nshmp-apps/src/app/hazard/disagg/components/plots/plots.component.ts +++ b/projects/nshmp-apps/src/app/hazard/disagg/components/plots/plots.component.ts @@ -1,11 +1,12 @@ import {AsyncPipe} from '@angular/common'; import {Component} from '@angular/core'; -import {ReactiveFormsModule} from '@angular/forms'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {MatButton} from '@angular/material/button'; import {MatOption} from '@angular/material/core'; import {MatDivider} from '@angular/material/divider'; import {MatFormField, MatLabel} from '@angular/material/form-field'; import {MatSelect} from '@angular/material/select'; +import {MatSliderModule} from '@angular/material/slider'; import {NshmpLibNgHazardDisaggPlotComponent} from '@ghsc/nshmp-lib-ng/hazard'; import {NshmpTemplateService} from '@ghsc/nshmp-template'; @@ -25,6 +26,8 @@ import {AppService} from '../../services/app.service'; MatOption, AsyncPipe, ReactiveFormsModule, + MatSliderModule, + FormsModule, ], selector: 'app-plots', standalone: true, diff --git a/projects/nshmp-apps/src/app/hazard/disagg/models/state.model.ts b/projects/nshmp-apps/src/app/hazard/disagg/models/state.model.ts index 00a8de0fb470611f5e477747cde2e81f0a3180fd..c25958222a71f9796effafa1df9877d6a592e608 100644 --- a/projects/nshmp-apps/src/app/hazard/disagg/models/state.model.ts +++ b/projects/nshmp-apps/src/app/hazard/disagg/models/state.model.ts @@ -2,7 +2,9 @@ import {ResponseSpectra} from '@ghsc/nshmp-lib-ng/hazard'; import {ServiceCallInfo} from '@ghsc/nshmp-lib-ng/nshmp'; import {NshmpPlot} from '@ghsc/nshmp-lib-ng/plot'; import { + DisaggComponentData, DisaggResponse, + DisaggResponseDataValues, DisaggUsage, } from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/disagg-service'; import {NshmMetadata} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/nshm-service'; @@ -14,6 +16,10 @@ import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata'; export interface AppState { /** Available NSHMs */ availableModels: Parameter[]; + /** Current component data */ + componentData: DisaggComponentData; + /** Current disagg data */ + disaggData: DisaggResponseDataValues; /** Fault sections */ faults: GeoJSON.FeatureCollection; /** NSHM service metadata */ diff --git a/projects/nshmp-apps/src/app/hazard/disagg/services/app.service.ts b/projects/nshmp-apps/src/app/hazard/disagg/services/app.service.ts index a65a692aa720d9963661cadb5b95f3d164902873..cb0cfdb8e41cc57fe4fd989232d9a7b3e9936ad9 100644 --- a/projects/nshmp-apps/src/app/hazard/disagg/services/app.service.ts +++ b/projects/nshmp-apps/src/app/hazard/disagg/services/app.service.ts @@ -1,15 +1,26 @@ import {HttpClient} from '@angular/common/http'; -import {computed, Injectable, Signal, signal} from '@angular/core'; +import { + computed, + Inject, + Injectable, + LOCALE_ID, + Signal, + signal, +} from '@angular/core'; import {AbstractControl, FormBuilder, Validators} from '@angular/forms'; import {ActivatedRoute, Router} from '@angular/router'; import { DisaggControlForm, DisaggTarget, + FormatLatitudePipe, + FormatLongitudePipe, HazardService, hazardUtils, + ReturnPeriodPipe, } from '@ghsc/nshmp-lib-ng/hazard'; import { NshmpService, + nshmpUtils, RETURN_PERIOD_BOUNDS, ReturnPeriod, ServiceCallInfo, @@ -70,11 +81,16 @@ export class AppService private hazardService: HazardService, private route: ActivatedRoute, private router: Router, - private http: HttpClient + private http: HttpClient, + @Inject(LOCALE_ID) private localId: string ) { super(); this.addValidators(); this.formGroup.controls.disaggComponent.disable(); + + this.formGroup.controls.disaggComponent.valueChanges.subscribe(() => + this.updateComponentData() + ); } /** @@ -88,38 +104,14 @@ export class AppService * Returns the disagg response values observable. */ get componentData(): Signal<DisaggComponentData> { - return computed(() => { - const {serviceResponse} = this.state(); - - if (serviceResponse === null) { - return null; - } - - return [...serviceResponse.response.disaggs] - .pop() - .data.find( - disagg => - disagg.component === this.formGroup.getRawValue().disaggComponent - ); - }); + return computed(() => this.state().componentData); } /** * Returns the disaggregation data observable. */ get disaggData(): Signal<DisaggResponseDataValues> { - return computed(() => { - const {serviceResponse} = this.state(); - - if (serviceResponse === null) { - return null; - } - - return serviceResponse.response.disaggs.find( - disagg => - disagg.imt.value === this.formGroup.getRawValue().imt.toString() - ); - }); + return computed(() => this.state().disaggData); } get faults(): Signal<GeoJSON.FeatureCollection> { @@ -237,6 +229,9 @@ export class AppService serviceResponse, }); + this.updateComponentData(); + this.updateDisaggData(); + this.formGroup.controls.disaggComponent.enable(); }); } @@ -301,6 +296,8 @@ export class AppService return { availableModels: [], + componentData: null, + disaggData: null, faults: null, nshmServices: [], plots: null, @@ -367,6 +364,17 @@ export class AppService this.nshmpService.saveAs(blob, `disagg-${data.component}.csv`); } + saveComponentSummaryReport(): void { + const blob = new Blob([this.summaryReportText(this.componentData())], { + type: 'text/csv;charset=utf-8;', + }); + + this.nshmpService.saveAs( + blob, + `disagg-summary-${this.componentData().component}.txt` + ); + } + /** * Save the contributions. * @@ -396,6 +404,18 @@ export class AppService this.nshmpService.saveAs(blob, `disagg-summary-${data.component}.txt`); } + saveSummaryReport(): void { + const summaries = this.disaggData().data.map((data, index) => + this.summaryReportText(data, index === 0) + ); + + const blob = new Blob(summaries, { + type: 'text/csv;charset=utf-8', + }); + + this.nshmpService.saveAs(blob, 'dissag-summary.txt'); + } + /** * Set the location form fields. * @@ -408,6 +428,19 @@ export class AppService }); } + updateComponentData(): void { + this.updateDisaggData(); + + const componentData = this.disaggData()?.data.find( + disagg => + disagg.component === this.formGroup.getRawValue().disaggComponent + ); + + this.updateState({ + componentData, + }); + } + updateState(state: Partial<AppState>): void { const updatedState = { ...this.state(), @@ -446,20 +479,39 @@ export class AppService */ private componentDataToCSV( componentData: DisaggComponentData, - formValues: DisaggControlForm + formValues: DisaggControlForm, + showMetadata = true ): string { + const metadata = this.serviceResponse().response.metadata; + const bins = metadata.εbins; + const keys = componentData.summary.find( + data => data.name.toLowerCase() === 'epsilon keys' + ); + + const headers = [ + 'Distance (km)', + 'Magnitude (Mw)', + 'ε total', + ...keys.data.map(data => data.name), + ]; + const csv = componentData.data.map(data => { - const εbin = data.εdata.map(εdata => εdata.εbin).join(', '); - const values = data.εdata.map(εdata => εdata.value).join(', '); - return ( - 'r, m, rBar, mBar, εBar\n' + - `${data.r}, ${data.m}, ${data.rBar}, ${data.mBar}, ${data.εBar}\n\n` + - `εbin, ${εbin}\n` + - `Value, ${values}\n` + const εvalues = bins.map( + bin => data.εdata.find(data => data.εbin === bin.id)?.value ?? 0.0 ); + const sum = εvalues.reduce((a, b) => a + b, 0); + const values = [sum, ...εvalues] + .map(num => num.toExponential(3)) + .join(', '); + + return `${data.r}, ${data.m}, ${values}`; }); - return `${this.metadataCSV(formValues)}\n\n\n` + csv.join('\n\n'); + const table = `${headers.join(', ')}\n${csv.join('\n')}`; + + return showMetadata + ? `${this.metadataCSV(formValues)}\n\n\n` + table + : table; } /** @@ -470,10 +522,13 @@ export class AppService */ private contributorsToCSV( sources: DisaggSource[], - formValues: DisaggControlForm + formValues: DisaggControlForm, + showMetadata = true ): string { + const label = 'Disaggregation Contributions'; + if (sources === null || sources.length === 0) { - throw new Error('No contributing sources'); + return `${label}: N/A\n\n`; } const headers = 'Source Set ↳ Source, Type, r, m, ε0, lon, lat, az, %'; @@ -488,13 +543,12 @@ export class AppService }) .map(source => source.replace(/null/gi, '')) .join(''); - return ( - '' + - `${this.metadataCSV(formValues)}\n\n` + - 'Disaggregation Contributions\n' + - `${headers}\n` + - `${csv}` - ); + + const table = `${label}\n${headers}\n ${csv}\n\n`; + + return showMetadata + ? `${this.metadataCSV(formValues)}\n\n ${table}` + : table; } private initialFormSet(): void { @@ -570,6 +624,28 @@ export class AppService ); } + private metadataText(formValues: DisaggControlForm): string { + const usage = this.usage().response; + + const imt = usage.model.imts.find( + imt => imt.value === formValues.imt.toString() + )?.display; + + const vs30 = + formValues.siteClass === nshmpUtils.selectPlaceHolder().value + ? `${formValues.vs30} m/s` + : `${formValues.vs30} m/s (${formValues.siteClass})`; + + return ( + `Model: ${usage.model.name}\n` + + `Longitude: ${new FormatLongitudePipe(this.localId).transform(formValues.longitude)} \n` + + `Latitude: ${new FormatLatitudePipe(this.localId).transform(formValues.latitude)} \n` + + `IMT: ${imt} \n` + + `Return Period: ${new ReturnPeriodPipe().transform(formValues.returnPeriod)} \n` + + `VS30: ${vs30} \n\n` + ); + } + /** * Build the URL to call the appropriate backend service with the given values. * @@ -602,6 +678,27 @@ export class AppService } } + private summaryReportText( + componentData: DisaggComponentData, + showMetadata = true + ): string { + const formValues = this.formGroup.getRawValue(); + const firstLine = '*** Disaggregation of Seismic Hazard ***'; + const metadata = showMetadata + ? `${firstLine}\n\n${this.metadataText(formValues)}` + : ''; + + return ( + metadata + + `** Disaggregation Component: ${componentData.component} **\n\n` + + this.summaryToText(componentData.summary, formValues, false) + + '\n\n' + + this.componentDataToCSV(componentData, formValues, false) + + '\n\n' + + this.contributorsToCSV(componentData.sources, formValues, false) + ); + } + /** * Convert the disaggregation summaries to text. * @@ -610,7 +707,8 @@ export class AppService */ private summaryToText( summaries: DisaggSummary[], - formValues: DisaggControlForm + formValues: DisaggControlForm, + showMetadata = true ): string { const text = summaries.map(summary => { const name = summary.name; @@ -621,7 +719,19 @@ export class AppService return `${name}:\n` + values.join('\n'); }); - return `${this.metadataCSV(formValues)}\n\n\n` + `${text.join('\n\n\n')}`; + return showMetadata + ? `${this.metadataCSV(formValues)}\n\n\n` + `${text.join('\n\n')}` + : `${text.join('\n\n')}`; + } + + private updateDisaggData(): void { + const disaggData = this.state().serviceResponse?.response.disaggs.find( + disagg => disagg.imt.value === this.formGroup.getRawValue().imt.toString() + ); + + this.updateState({ + disaggData, + }); } private updateUrl(): void { @@ -644,7 +754,7 @@ export class AppService this.updateState({ serviceCallInfo: { ...this.state().serviceCallInfo, - usage: [nshmService.url], + usage: [`${nshmService.url}${this.endpoint}`], }, }); } diff --git a/projects/nshmp-apps/src/styles/_print.scss b/projects/nshmp-apps/src/styles/_print.scss index b033dcec23e07a4930366d63343e1e6ef0aea966..2103c6e0d5443850bf03872b229fa84d3f9e0674 100644 --- a/projects/nshmp-apps/src/styles/_print.scss +++ b/projects/nshmp-apps/src/styles/_print.scss @@ -23,6 +23,14 @@ page-break-before: always !important; } + .print-full-page { + height: 10in; + } + + mat-tab-header { + display: none !important; + } + mat-accordion { mat-expansion-panel { .mat-expansion-indicator {