services/hazard-plots.service.ts
Dash type for models
Properties |
modelA |
modelA:
|
Type : Dash
|
Optional |
Dash type for model A |
modelB |
modelB:
|
Type : Dash
|
Optional |
Dash type for model B |
import {Injectable} from '@angular/core';
import {hazardUtils} from '@ghsc/nshmp-lib-ng/hazard';
import {FormGroupControls, NumberBounds} from '@ghsc/nshmp-lib-ng/nshmp';
import {NshmpPlot, plotUtils} from '@ghsc/nshmp-lib-ng/plot';
import {
HazardCurve,
HazardResponse,
} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/hazard-service';
import {XySequence} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/data';
import {Imt, imtToPeriod} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
import {
SourceType,
sourceTypeToPascalCase,
} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/model';
import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
import {PlotlyPlot} from '@ghsc/nshmp-utils-ts/libs/plotly';
import {Dash, PlotData} from 'plotly.js';
import {
AppState,
ControlForm,
Plots,
ServiceResponse,
ServiceResponses,
} from '../models/state.model';
import {MathService} from './math.service';
/**
* Base options for creating hazard plots
*/
interface BaseCreateHazardPlotOptions {
/** Current avialable models */
availableModels: Parameter[];
/** Control panel form values */
form: ControlForm;
/** Function to filter hazard curve data */
hazardCurveFilter: (data: HazardCurve) => boolean;
/** Function to return the name of the data */
name: (modelInfo: Parameter, data: HazardCurve) => string;
/** Whether to reset the color of the line per model */
resetColor: boolean;
}
/**
* Options for creating hazard plot data.
*/
interface CreateHazardPlotDataOptions extends BaseCreateHazardPlotOptions {
/** The service response for a model */
serviceResponse: ServiceResponse;
/** The dash type */
dash?: Dash;
}
/**
* Options for creating a hazard plot.
*/
interface CreateHazardPlotOptions extends BaseCreateHazardPlotOptions {
/** Plot id */
id: Plots;
/** The current plot */
plot: NshmpPlot;
/** The service responses */
serviceResponses: ServiceResponses;
/** Dash types per model */
dash?: PlotDash;
}
/**
* Dash type for models
*/
interface PlotDash {
/** Dash type for model A */
modelA?: Dash;
/** Dash type for model B */
modelB?: Dash;
}
@Injectable({
providedIn: 'root',
})
export class HazardPlotsService {
constructor(private mathService: MathService) {}
/**
* Create the plots.
*
* @param state The app state
*/
createHazardPlots(
state: AppState,
formGroup: FormGroupControls<ControlForm>,
): Map<string, NshmpPlot> {
const plots = new Map<string, NshmpPlot>();
const hazardPlot = state.plots.get(Plots.HAZARD);
const hazardComponentsPlot = state.plots.get(Plots.HAZARD_COMPONENTS);
const hazardDiffPlot = state.plots.get(Plots.HAZARD_DIFFERENCES);
const hazardCurves = this.createHazardCurvesPlot(
state.serviceResponses,
state.availableModels,
hazardPlot,
formGroup.getRawValue(),
);
plots.set(Plots.HAZARD, {
...hazardPlot,
plotData: hazardCurves,
});
const hazardCurveDifference = this.createHazardDiffPlot(
state.serviceResponses,
formGroup.getRawValue().imt,
hazardDiffPlot,
);
plots.set(Plots.HAZARD_DIFFERENCES, {
...hazardDiffPlot,
plotData: hazardCurveDifference,
});
const hazardCurveComponents = this.createHazardComponentsPlot(
state.serviceResponses,
state.availableModels,
hazardComponentsPlot,
formGroup.getRawValue(),
);
plots.set(Plots.HAZARD_COMPONENTS, {
...hazardComponentsPlot,
plotData: hazardCurveComponents,
});
return plots;
}
/**
* Create a new Plotly plot from the current model responses.
*
* @param serviceResponses The service responses for both models
* @param imt The control panel IMT value
* @param plot The current hazard difference plot
*/
private createHazardDiffPlot(
serviceResponses: ServiceResponses,
imt: Imt,
plot: NshmpPlot,
): PlotlyPlot {
const data = this.createHazardDiffPlotData(serviceResponses, imt);
const yValues = data.y as number[];
const yMax = Math.max(...yValues.map(y => Math.abs(y))) * 1.3;
const yRange = [-yMax, yMax];
const {layout, mobileLayout} = plot.plotData;
return {
config: {...plot.plotData.config},
data: [data],
id: Plots.HAZARD_DIFFERENCES,
layout: {
...layout,
yaxis: {
...layout.yaxis,
autorange: false,
range: yRange,
},
},
mobileConfig: {...plot.plotData.mobileConfig},
mobileLayout: {
...mobileLayout,
yaxis: {
...mobileLayout.yaxis,
autorange: false,
range: yRange,
},
},
};
}
/**
* Returns the Plotly plot data associated with the service responses
* for hazard difference plot.
*
* @param serviceResponses The service responses for both models
* @param imt The control panel IMT value
*/
private createHazardDiffPlotData(
serviceResponses: ServiceResponses,
imt: Imt,
): Partial<PlotData> {
const {modelA, modelB} = serviceResponses;
const modelAResponse = this.getHazardResponse(modelA, imt);
const modelBResponse = this.getHazardResponse(modelB, imt);
const modelAData = hazardUtils.getSourceTypeData(
modelAResponse.data,
sourceTypeToPascalCase(SourceType.TOTAL),
);
const modelBData = hazardUtils.getSourceTypeData(
modelBResponse.data,
sourceTypeToPascalCase(SourceType.TOTAL),
);
const xValues = modelAData.values.xs.filter(x =>
modelBData.values.xs.includes(x),
);
const yValues = this.mathService.percentDifferences(
this.filterYValues(xValues, modelAData.values),
this.filterYValues(xValues, modelBData.values),
);
return {
hovertemplate: '%{x} g, %{y} %',
mode: 'lines+markers',
name: '% Difference',
showlegend: false,
x: xValues,
y: yValues,
};
}
/**
* Create the hazard curves plot.
*
* @param serviceResponses The model responses
* @param availableModels The available models
* @param plot The current hazard plot
* @param form The form values
*/
private createHazardCurvesPlot(
serviceResponses: ServiceResponses,
availableModels: Parameter[],
plot: NshmpPlot,
form: ControlForm,
): PlotlyPlot {
const imt = form.imt;
const imtStr =
imt === Imt.PGA || imt === Imt.PGV
? imt
: `${imtToPeriod(imt)} s ${imt.substring(0, 2)}`;
const hazardCurveFilter = (data: HazardCurve) =>
data.component === sourceTypeToPascalCase(SourceType.TOTAL);
const name = (modelInfo: Parameter) => `${modelInfo.display} - ${imtStr}`;
return this.createHazardPlot({
availableModels,
form,
hazardCurveFilter,
id: Plots.HAZARD,
name,
plot,
resetColor: false,
serviceResponses,
});
}
/**
* Create the hazard components plot.
*
* @param serviceResponses The model responses
* @param availableModels The available models
* @param plot The current hazard plot
* @param form The form values
*/
private createHazardComponentsPlot(
serviceResponses: ServiceResponses,
availableModels: Parameter[],
plot: NshmpPlot,
form: ControlForm,
): PlotlyPlot {
const hazardCurveFilter = (data: HazardCurve) =>
data.component !== sourceTypeToPascalCase(SourceType.TOTAL);
const name = (modelInfo: Parameter, data: HazardCurve) =>
`${modelInfo.display} - ${data.component}`;
return this.createHazardPlot({
availableModels,
dash: {
modelA: 'solid',
modelB: 'dash',
},
form,
hazardCurveFilter,
id: Plots.HAZARD_COMPONENTS,
name,
plot,
resetColor: true,
serviceResponses,
});
}
/**
* Returns a `PlotlyPlot` for a hazard plot.
*
* @param options The hazard plot options
*/
private createHazardPlot(options: CreateHazardPlotOptions): PlotlyPlot {
const dataA = this.createHazardPlotData({
...options,
dash: options.dash?.modelA,
serviceResponse: options.serviceResponses.modelA,
});
const dataB = this.createHazardPlotData({
...options,
dash: options.dash?.modelB,
serviceResponse: options.serviceResponses.modelB,
});
const limitsA = this.hazardCurveLimits(
options.serviceResponses.modelA,
options.form.imt,
options.hazardCurveFilter,
);
const limitsB = this.hazardCurveLimits(
options.serviceResponses.modelB,
options.form.imt,
options.hazardCurveFilter,
);
const xMin = Math.min(limitsA.min, limitsB.min);
const xMax = Math.min(limitsA.max, limitsB.max);
const returnPeriodData = this.createReturnPeriodPlotData(
options.form.returnPeriod,
[xMin, xMax],
);
const metadata =
options.serviceResponses.modelA.hazardResponse.response.metadata;
const layout = plotUtils.updatePlotLabels({
layout: options.plot.plotData.layout,
xLabel: metadata.xlabel,
yLabel: metadata.ylabel,
});
const mobileLayout = plotUtils.updatePlotLabels({
layout: options.plot.plotData.mobileLayout,
xLabel: metadata.xlabel,
yLabel: metadata.ylabel,
});
return {
config: {...options.plot.plotData.config},
data: [returnPeriodData, ...dataA, ...dataB],
id: options.id,
layout,
mobileConfig: {...options.plot.plotData.mobileConfig},
mobileLayout,
};
}
/**
* Create the plot data for hazard component plot.
*
* @param options The plot data options
*/
private createHazardPlotData(
options: CreateHazardPlotDataOptions,
): Partial<PlotData>[] {
const imt = options.form.imt;
const hazardCurves = this.getHazardResponse(
options.serviceResponse,
imt,
).data.filter(data => options.hazardCurveFilter(data));
const modelInfo = this.mathService.getModelInfo(
options.serviceResponse.model,
options.availableModels,
);
const colors = plotUtils.COLORWAY;
let count = 0;
return hazardCurves.map(data => {
const color = colors[count++ % colors.length];
const xy = hazardUtils.updateXySequence(
options.form,
hazardUtils.cleanXySequence(data.values),
imt,
);
const plotlyData: Partial<PlotData> = {
hovertemplate: '%{x} g, %{y} AFE',
line: {
color: options.resetColor ? color : undefined,
dash: options.dash,
},
mode: 'lines+markers',
name: options.name(modelInfo, data),
x: xy.xs,
y: xy.ys,
};
return plotlyData;
});
}
/**
* Returns the plot data for the return period.
*
* @param returnPeriod Retrun period
* @param xLimits The X limits
*/
private createReturnPeriodPlotData(
returnPeriod: number,
xLimits: number[],
): Partial<PlotData> {
return {
hoverinfo: 'none',
line: {
color: 'black',
},
mode: 'lines',
name: `${returnPeriod} yr.`,
x: xLimits,
y: [1 / returnPeriod, 1 / returnPeriod],
};
}
/**
* Returns the Y values associated with the common X values.
*
* @param xValues The common X values
* @param xySequence The sequence
*/
private filterYValues(xValues: number[], xySequence: XySequence) {
const ys = xySequence.xs
.map((x, i) => [x, xySequence.ys[i]])
.filter(([x]) => xValues.includes(x))
.map(([, y]) => y);
return ys;
}
/**
* Returns the hazard response associated with a IMT.
*
* @param serviceResponse The service response for a single model
* @param imt The control panel IMT value
*/
private getHazardResponse(
serviceResponse: ServiceResponse,
imt: Imt,
): HazardResponse {
const hazardResponse =
serviceResponse.hazardResponse.response.hazardCurves.find(
curves => curves.imt.value === imt.toString(),
);
if (hazardResponse === undefined) {
throw new Error(`Service response does not contain the IMT ${imt}`);
}
return hazardResponse;
}
/**
* Returns the X min and max value for a service reponse.
*
* @param serviceResponse The service response
* @param imt The imt value
* @param hazardCurveFilter Function to filter data
*/
private hazardCurveLimits(
serviceResponse: ServiceResponse,
imt: Imt,
hazardCurveFilter: (data: HazardCurve) => boolean,
): NumberBounds {
const hazardCurves = this.getHazardResponse(
serviceResponse,
imt,
).data.filter(data => hazardCurveFilter(data));
const data = hazardCurves.map(curve => curve.values);
const min = Math.min(...data.map(xy => Math.min(...xy.xs)));
const max = Math.max(...data.map(xy => Math.max(...xy.xs)));
return {
max,
min,
};
}
}