diff --git a/frontend/src/modules/ParameterDistributionMatrix/settings/settings.tsx b/frontend/src/modules/ParameterDistributionMatrix/settings/settings.tsx index 0ce543a60..ce74e062f 100644 --- a/frontend/src/modules/ParameterDistributionMatrix/settings/settings.tsx +++ b/frontend/src/modules/ParameterDistributionMatrix/settings/settings.tsx @@ -38,6 +38,10 @@ export function Settings({ settingsContext, workbenchSession }: ModuleSettingsPr const [selectedVisualizationType, setSelectedVisualizationType] = settingsContext.useSettingsToViewInterfaceState("selectedVisualizationType"); + const [showIndividualRealizationValues, setShowIndividualRealizationValues] = + settingsContext.useSettingsToViewInterfaceState("showIndividualRealizationValues"); + const [showPercentilesAndMeanLines, setShowPercentilesAndMeanLines] = + settingsContext.useSettingsToViewInterfaceState("showPercentilesAndMeanLines"); function handleEnsembleSelectionChange(ensembleIdents: EnsembleIdent[]) { setSelectedEnsembleIdents(ensembleIdents); @@ -55,6 +59,12 @@ export function Settings({ settingsContext, workbenchSession }: ModuleSettingsPr function handleVisualizationTypeChange(event: React.ChangeEvent) { setSelectedVisualizationType(event.target.value as ParameterDistributionPlotType); } + function handleShowIndividualRealizationValuesChange(_: React.ChangeEvent, checked: boolean) { + setShowIndividualRealizationValues(checked); + } + function handleShowPercentilesAndMeanLinesChange(_: React.ChangeEvent, checked: boolean) { + setShowPercentilesAndMeanLines(checked); + } return (
@@ -67,6 +77,21 @@ export function Settings({ settingsContext, workbenchSession }: ModuleSettingsPr onChange={handleVisualizationTypeChange} /> + +
+ {"Show additional data"} + + +
+
= { baseStates: { selectedVisualizationType: ParameterDistributionPlotType.DISTRIBUTION_PLOT, + showIndividualRealizationValues: false, + showPercentilesAndMeanLines: false, }, derivedStates: { selectedEnsembleIdents: (get) => { diff --git a/frontend/src/modules/ParameterDistributionMatrix/typesAndEnums.ts b/frontend/src/modules/ParameterDistributionMatrix/typesAndEnums.ts index 6e7586d2c..9ea929681 100644 --- a/frontend/src/modules/ParameterDistributionMatrix/typesAndEnums.ts +++ b/frontend/src/modules/ParameterDistributionMatrix/typesAndEnums.ts @@ -1,12 +1,13 @@ import { ParameterIdent } from "@framework/EnsembleParameters"; -export type EnsembleParameterValues = { +export type EnsembleParameterRealizationsAndValues = { ensembleDisplayName: string; + realizations: number[]; values: number[]; }; export type ParameterDataArr = { parameterIdent: ParameterIdent; - ensembleParameterValues: EnsembleParameterValues[]; + ensembleParameterRealizationAndValues: EnsembleParameterRealizationsAndValues[]; }; export enum ParameterDistributionPlotType { diff --git a/frontend/src/modules/ParameterDistributionMatrix/view/components/ParameterDistributionPlot.tsx b/frontend/src/modules/ParameterDistributionMatrix/view/components/ParameterDistributionPlot.tsx index 386cad7ea..88da5f656 100644 --- a/frontend/src/modules/ParameterDistributionMatrix/view/components/ParameterDistributionPlot.tsx +++ b/frontend/src/modules/ParameterDistributionMatrix/view/components/ParameterDistributionPlot.tsx @@ -1,6 +1,8 @@ import React from "react"; import Plot from "react-plotly.js"; +import { computeQuantile } from "@modules_shared/statistics"; + import { PlotType } from "plotly.js"; import { ParameterDataArr, ParameterDistributionPlotType } from "../../typesAndEnums"; @@ -9,64 +11,157 @@ type ParameterDistributionPlotProps = { dataArr: ParameterDataArr[]; ensembleColors: Map; plotType: ParameterDistributionPlotType; + showIndividualRealizationValues: boolean; + showPercentilesAndMeanLines: boolean; width: number; height: number; }; -function convertToPlotlyPlotType(plotType: ParameterDistributionPlotType): PlotType { - if (plotType == ParameterDistributionPlotType.BOX_PLOT) { - return "box" as PlotType; - } - if (plotType == ParameterDistributionPlotType.DISTRIBUTION_PLOT) { - return "violin" as PlotType; - } - throw new Error(`Unknown plot type: ${plotType}`); -} - export const ParameterDistributionPlot: React.FC = (props) => { const numSubplots = props.dataArr.length; const numColumns = Math.ceil(Math.sqrt(numSubplots)); const numRows = Math.ceil(numSubplots / numColumns); const addedLegendNames: Set = new Set(); - function generateTraces(): any { - const traces: any = []; + const showRugTraces = + props.plotType == ParameterDistributionPlotType.DISTRIBUTION_PLOT && props.showIndividualRealizationValues; + function generateDistributionPlotTraces(): any[] { + const traces: any[] = []; let subplotIndex = 1; - const convertedPlotType = convertToPlotlyPlotType(props.plotType); - const hoverInfo = props.plotType == ParameterDistributionPlotType.BOX_PLOT ? "" : "none"; + props.dataArr.forEach((parameterData) => { + parameterData.ensembleParameterRealizationAndValues.forEach((ensembleData, index) => { + const shouldShowLegend = !addedLegendNames.has(ensembleData.ensembleDisplayName); + if (shouldShowLegend) { + addedLegendNames.add(ensembleData.ensembleDisplayName); + } + const ensembleColor = props.ensembleColors.get(ensembleData.ensembleDisplayName); + + const distributionTrace = { + x: ensembleData.values, + type: "violin" as PlotType, + spanmode: "hard", + name: ensembleData.ensembleDisplayName, + legendgroup: ensembleData.ensembleDisplayName, + marker: { color: ensembleColor }, + xaxis: `x${subplotIndex}`, + yaxis: `y${subplotIndex}`, + showlegend: shouldShowLegend, + y0: 0, + hoverinfo: "none", + meanline: { visible: true }, + orientation: "h", + side: "positive", + width: 2, + points: false, + }; + traces.push(distributionTrace); + + if (props.showPercentilesAndMeanLines) { + const yPosition = 0; + traces.push( + ...createQuantileAndMeanMarkerTraces( + ensembleData.values, + yPosition, + ensembleData.ensembleDisplayName, + ensembleColor, + subplotIndex + ) + ); + } + + if (props.showIndividualRealizationValues) { + const hoverText = ensembleData.values.map( + (_, index) => `Realization: ${ensembleData.realizations[index]}` + ); + + // Distribution plot shows positive values, thus the rug plot is placed below 0. + // Align the realization values horizontally below the distribution plot + const yPosition = -0.1 - index * 0.1; // Offset -0.1, and 0.1 between each ensemble + const yValues = ensembleData.values.map(() => yPosition); // Align horizontally with same y-position + + const rugTrace = { + x: ensembleData.values, // Use the same x values as your main trace + y: yValues, + type: "rug", + name: ensembleData.ensembleDisplayName, + legendgroup: ensembleData.ensembleDisplayName, + xaxis: `x${subplotIndex}`, + yaxis: `y${subplotIndex}`, + hovertext: hoverText, + hoverinfo: "x+text+name", + mode: "markers", + marker: { + color: props.ensembleColors.get(ensembleData.ensembleDisplayName), + symbol: "line-ns-open", + }, + showlegend: false, + }; + traces.push(rugTrace); + } + }); + + subplotIndex++; + }); + + return traces; + } + + function generateBoxPlotTraces(): any[] { + const traces: any[] = []; + let subplotIndex = 1; props.dataArr.forEach((parameterData) => { - parameterData.ensembleParameterValues.forEach((ensembleValue, index) => { - const shouldShowLegend = !addedLegendNames.has(ensembleValue.ensembleDisplayName); + parameterData.ensembleParameterRealizationAndValues.forEach((ensembleData, index) => { + const shouldShowLegend = !addedLegendNames.has(ensembleData.ensembleDisplayName); if (shouldShowLegend) { - addedLegendNames.add(ensembleValue.ensembleDisplayName); + addedLegendNames.add(ensembleData.ensembleDisplayName); } - let verticalPosition = 0; - if (props.plotType == ParameterDistributionPlotType.BOX_PLOT) { - verticalPosition = index * (2 + 1); // 2 is the height of each box + 1 space + if (ensembleData.values.length !== ensembleData.realizations.length) { + throw new Error("Realizations and values must have the same length"); } + const ensembleColor = props.ensembleColors.get(ensembleData.ensembleDisplayName); + + const verticalPosition = index * (2 + 1); // 2 is the height of each box + 1 space + const hoverText = ensembleData.values.map( + (_, index) => `Realization: ${ensembleData.realizations[index]}` + ); + const trace = { - x: ensembleValue.values, - type: convertedPlotType, - name: ensembleValue.ensembleDisplayName, - legendgroup: ensembleValue.ensembleDisplayName, - marker: { color: props.ensembleColors.get(ensembleValue.ensembleDisplayName) }, + x: ensembleData.values, + type: "box", + name: ensembleData.ensembleDisplayName, + legendgroup: ensembleData.ensembleDisplayName, + marker: { color: ensembleColor }, xaxis: `x${subplotIndex}`, yaxis: `y${subplotIndex}`, showlegend: shouldShowLegend, y0: verticalPosition, - hoverinfo: hoverInfo, + hoverinfo: "x+text+name", + hovertext: hoverText, meanline_visible: true, orientation: "h", side: "positive", width: 2, points: false, + boxpoints: props.showIndividualRealizationValues ? "all" : "outliers", }; traces.push(trace); + + if (props.showPercentilesAndMeanLines) { + traces.push( + ...createQuantileAndMeanMarkerTraces( + ensembleData.values, + verticalPosition, + ensembleData.ensembleDisplayName, + ensembleColor, + subplotIndex + ) + ); + } }); subplotIndex++; }); @@ -74,6 +169,56 @@ export const ParameterDistributionPlot: React.FC return traces; } + function createQuantileAndMeanMarkerTraces( + parameterValues: number[], + yPosition: number, + ensembleName: string, + ensembleColor: string | undefined, + subplotIndex: number + ): any[] { + const p90 = computeQuantile(parameterValues, 0.9); + const p10 = computeQuantile(parameterValues, 0.1); + const mean = parameterValues.reduce((a, b) => a + b, 0) / parameterValues.length; + const p10Trace = { + x: [p10], + y: [yPosition], + type: "scatter", + hoverinfo: "x+text", + hovertext: "P10", + showlegend: false, + legendgroup: ensembleName, + xaxis: `x${subplotIndex}`, + yaxis: `y${subplotIndex}`, + marker: { color: ensembleColor, symbol: "x", size: 10 }, + }; + const meanTrace = { + x: [mean], + y: [yPosition], + type: "scatter", + hoverinfo: "x+text", + hovertext: "Mean", + showlegend: false, + legendgroup: ensembleName, + xaxis: `x${subplotIndex}`, + yaxis: `y${subplotIndex}`, + marker: { color: ensembleColor, symbol: "x", size: 10 }, + }; + const p90Trace = { + x: [p90], + y: [yPosition], + type: "scatter", + hoverinfo: "x+text", + hovertext: "P90", + showlegend: false, + legendgroup: ensembleName, + xaxis: `x${subplotIndex}`, + yaxis: `y${subplotIndex}`, + marker: { color: ensembleColor, symbol: "x", size: 10 }, + }; + + return [p10Trace, meanTrace, p90Trace]; + } + function generateLayout(): any { const layout: any = { height: props.height, @@ -89,18 +234,21 @@ export const ParameterDistributionPlot: React.FC title: props.dataArr[i - 1].parameterIdent, mirror: true, showline: true, + zeroline: false, linewidth: 1, linecolor: "black", }; + layout[`yaxis${i}`] = { showticklabels: false, showgrid: false, - zeroline: false, + zeroline: showRugTraces, mirror: true, showline: true, linewidth: 1, linecolor: "black", }; + layout.annotations.push({ text: props.dataArr[i - 1].parameterIdent.name, showarrow: false, @@ -114,7 +262,14 @@ export const ParameterDistributionPlot: React.FC return layout; } - const data = generateTraces(); + let data = []; + if (props.plotType == ParameterDistributionPlotType.DISTRIBUTION_PLOT) { + data = generateDistributionPlotTraces(); + } + if (props.plotType == ParameterDistributionPlotType.BOX_PLOT) { + data = generateBoxPlotTraces(); + } + const layout = generateLayout(); return ; diff --git a/frontend/src/modules/ParameterDistributionMatrix/view/view.tsx b/frontend/src/modules/ParameterDistributionMatrix/view/view.tsx index 1cd773c91..dac50edca 100644 --- a/frontend/src/modules/ParameterDistributionMatrix/view/view.tsx +++ b/frontend/src/modules/ParameterDistributionMatrix/view/view.tsx @@ -20,6 +20,12 @@ export function View(props: ModuleViewProps) { const selectedEnsembleIdents = props.viewContext.useSettingsToViewInterfaceValue("selectedEnsembleIdents"); const selectedParameterIdents = props.viewContext.useSettingsToViewInterfaceValue("selectedParameterIdents"); const selectedVisualizationType = props.viewContext.useSettingsToViewInterfaceValue("selectedVisualizationType"); + const showIndividualRealizationValues = props.viewContext.useSettingsToViewInterfaceValue( + "showIndividualRealizationValues" + ); + const showPercentilesAndMeanLines = + props.viewContext.useSettingsToViewInterfaceValue("showPercentilesAndMeanLines"); + const ensembleSet = props.workbenchSession.getEnsembleSet(); const filterEnsembleRealizationsFunc = useEnsembleRealizationFilterFunc(props.workbenchSession); @@ -43,6 +49,8 @@ export function View(props: ModuleViewProps) { dataArr={parameterDataArr} ensembleColors={ensembleColors} plotType={selectedVisualizationType} + showIndividualRealizationValues={showIndividualRealizationValues} + showPercentilesAndMeanLines={showPercentilesAndMeanLines} width={wrapperDivSize.width} height={wrapperDivSize.height} > @@ -61,7 +69,7 @@ function makeParameterDataArr( for (const parameterIdent of parameterIdents) { const parameterDataArrEntry: ParameterDataArr = { parameterIdent: parameterIdent, - ensembleParameterValues: [], + ensembleParameterRealizationAndValues: [], }; for (const ensembleIdent of ensembleIdents) { @@ -71,28 +79,28 @@ function makeParameterDataArr( const ensembleParameters = ensemble.getParameters(); if (!ensembleParameters.hasParameter(parameterIdent)) continue; - const parameter = ensembleParameters.getParameter(parameterIdent); - const filteredRealizations = new Set(filterEnsembleRealizations(ensembleIdent)); + const parameter = ensembleParameters.getParameter(parameterIdent); - const parameterValues = parameter.realizations - .map((realization, index) => { - if (filteredRealizations.has(realization)) { - return parameter.values[index] as number; - } - return null; - }) - .filter((value) => value !== null); + const parameterValues: number[] = []; + const realizationNumbers: number[] = []; + parameter.realizations.forEach((realization, index) => { + if (filteredRealizations.has(realization)) { + parameterValues.push(parameter.values[index] as number); + realizationNumbers.push(realization); + } + }); const ensembleParameterValues = { ensembleDisplayName: ensemble.getDisplayName(), - values: parameterValues as number[], + values: parameterValues, + realizations: realizationNumbers, }; - parameterDataArrEntry.ensembleParameterValues.push(ensembleParameterValues); + parameterDataArrEntry.ensembleParameterRealizationAndValues.push(ensembleParameterValues); } - if (parameterDataArrEntry.ensembleParameterValues.length > 0) { + if (parameterDataArrEntry.ensembleParameterRealizationAndValues.length > 0) { parameterDataArr.push(parameterDataArrEntry); } } diff --git a/frontend/src/modules/_shared/statistics.ts b/frontend/src/modules/_shared/statistics.ts index 53cd8016f..021c679e2 100644 --- a/frontend/src/modules/_shared/statistics.ts +++ b/frontend/src/modules/_shared/statistics.ts @@ -7,7 +7,7 @@ export const computeQuantile = (data: number[], quantile: number): number => { if (data.length === 0) { return 0; } - if (data.length === 0) { + if (data.length === 1) { return data[0]; } const sortedValues = data.sort((a, b) => a - b); @@ -24,4 +24,4 @@ export const computeQuantile = (data: number[], quantile: number): number => { const fraction = rank - lowerRank; return sortedValues[lowerRank] * (1 - fraction) + sortedValues[lowerRank + 1] * fraction; } -} \ No newline at end of file +};