diff --git a/frontend/src/framework/Ensemble.ts b/frontend/src/framework/Ensemble.ts index e3cb7d4d2..146e6857c 100644 --- a/frontend/src/framework/Ensemble.ts +++ b/frontend/src/framework/Ensemble.ts @@ -1,6 +1,6 @@ import { EnsembleIdent } from "./EnsembleIdent"; -import { EnsembleSensitivities, Sensitivity } from "./EnsembleSensitivities"; import { EnsembleParameters, Parameter } from "./EnsembleParameters"; +import { EnsembleSensitivities, Sensitivity } from "./EnsembleSensitivities"; export class Ensemble { private _ensembleIdent: EnsembleIdent; diff --git a/frontend/src/modules/SimulationTimeSeriesMatrix/loadModule.tsx b/frontend/src/modules/SimulationTimeSeriesMatrix/loadModule.tsx index 5faaf23d9..925579edb 100644 --- a/frontend/src/modules/SimulationTimeSeriesMatrix/loadModule.tsx +++ b/frontend/src/modules/SimulationTimeSeriesMatrix/loadModule.tsx @@ -7,6 +7,8 @@ import { view } from "./view"; const defaultState: State = { groupBy: GroupBy.TIME_SERIES, + colorRealizationsByParameter: false, + parameterIdent: null, visualizationMode: VisualizationMode.INDIVIDUAL_REALIZATIONS, vectorSpecifications: [], resamplingFrequency: Frequency_api.MONTHLY, diff --git a/frontend/src/modules/SimulationTimeSeriesMatrix/settings.tsx b/frontend/src/modules/SimulationTimeSeriesMatrix/settings.tsx index e0fa2a075..7bd8d34b0 100644 --- a/frontend/src/modules/SimulationTimeSeriesMatrix/settings.tsx +++ b/frontend/src/modules/SimulationTimeSeriesMatrix/settings.tsx @@ -1,7 +1,8 @@ import React from "react"; -import { Frequency_api, StatisticFunction_api, VectorDescription_api } from "@api"; +import { Frequency_api, StatisticFunction_api } from "@api"; import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { ParameterIdent, ParameterType } from "@framework/EnsembleParameters"; import { EnsembleSet } from "@framework/EnsembleSet"; import { ModuleFCProps } from "@framework/Module"; import { useEnsembleSet } from "@framework/WorkbenchSession"; @@ -16,6 +17,7 @@ import { Label } from "@lib/components/Label"; import { RadioGroup } from "@lib/components/RadioGroup"; import { SmartNodeSelectorSelection, TreeDataNode } from "@lib/components/SmartNodeSelector"; import { VectorSelector } from "@lib/components/VectorSelector"; +import { useValidState } from "@lib/hooks/useValidState"; import { resolveClassNames } from "@lib/utils/resolveClassNames"; import { createVectorSelectorDataFromVectors } from "@lib/utils/vectorSelectorUtils"; @@ -38,35 +40,28 @@ import { EnsembleVectorListsHelper } from "./utils/ensemblesVectorListHelper"; export function settings({ moduleContext, workbenchSession }: ModuleFCProps) { const ensembleSet = useEnsembleSet(workbenchSession); + + // Store state/values const [resampleFrequency, setResamplingFrequency] = moduleContext.useStoreState("resamplingFrequency"); const [groupBy, setGroupBy] = moduleContext.useStoreState("groupBy"); + const [colorRealizationsByParameter, setColorRealizationsByParameter] = + moduleContext.useStoreState("colorRealizationsByParameter"); const [visualizationMode, setVisualizationMode] = moduleContext.useStoreState("visualizationMode"); const [showHistorical, setShowHistorical] = moduleContext.useStoreState("showHistorical"); const [showObservations, setShowObservations] = moduleContext.useStoreState("showObservations"); const [statisticsSelection, setStatisticsSelection] = moduleContext.useStoreState("statisticsSelection"); + const setParameterIdent = moduleContext.useSetStoreValue("parameterIdent"); const setVectorSpecifications = moduleContext.useSetStoreValue("vectorSpecifications"); + // States const [previousEnsembleSet, setPreviousEnsembleSet] = React.useState(ensembleSet); const [selectedEnsembleIdents, setSelectedEnsembleIdents] = React.useState([]); const [selectedVectorNames, setSelectedVectorNames] = React.useState([]); const [vectorSelectorData, setVectorSelectorData] = React.useState([]); - const [prevVisualizationMode, setPrevVisualizationMode] = React.useState(visualizationMode); - if (prevVisualizationMode !== visualizationMode) { - setPrevVisualizationMode(visualizationMode); - } - const vectorListQueries = useVectorListQueries(selectedEnsembleIdents); - const ensembleVectorListsHelper = new EnsembleVectorListsHelper(selectedEnsembleIdents, vectorListQueries); - const vectorsUnion: VectorDescription_api[] = ensembleVectorListsHelper.vectorsUnion(); - - const selectedVectorNamesHasHistorical = ensembleVectorListsHelper.hasAnyHistoricalVector(selectedVectorNames); - const currentVectorSelectorData = createVectorSelectorDataFromVectors(vectorsUnion.map((vector) => vector.name)); - - // Only update if all vector lists are retrieved before updating vectorSelectorData has changed - const hasVectorListQueriesErrorOrLoading = vectorListQueries.some((query) => query.isLoading || query.isError); - if (!hasVectorListQueriesErrorOrLoading && !isEqual(currentVectorSelectorData, vectorSelectorData)) { - setVectorSelectorData(currentVectorSelectorData); + if (visualizationMode !== prevVisualizationMode) { + setPrevVisualizationMode(visualizationMode); } if (!isEqual(ensembleSet, previousEnsembleSet)) { @@ -81,6 +76,35 @@ export function settings({ moduleContext, workbenchSession }: ModuleFCProps param.equals(parameter))) continue; + if (ensemble.getParameters().getParameter(parameter).isConstant) continue; + + continuousAndNonConstantParametersUnion.push(parameter); + } + } + const [selectedParameterIdentStr, setSelectedParameterIdentStr] = useValidState(null, [ + continuousAndNonConstantParametersUnion, + (item: ParameterIdent) => item.toString(), + ]); + + // Await update of vectorSelectorData until all vector lists are retrieved + const hasVectorListQueriesErrorOrFetching = vectorListQueries.some((query) => query.isFetching || query.isError); + if (!hasVectorListQueriesErrorOrFetching && !isEqual(currentVectorSelectorData, vectorSelectorData)) { + setVectorSelectorData(currentVectorSelectorData); + } + React.useEffect( function propagateVectorSpecsToView() { const newVectorSpecifications: VectorSpec[] = []; @@ -97,16 +121,44 @@ export function settings({ moduleContext, workbenchSession }: ModuleFCProps + parameter.equals(newParameterIdent) + ); + if (isParameterInUnion) { + setParameterIdent(newParameterIdent); + } else { + setParameterIdent(null); + } + } catch { + setParameterIdent(null); + } + }, + [selectedParameterIdentStr] + ); + function handleGroupByChange(event: React.ChangeEvent) { setGroupBy(event.target.value as GroupBy); } + function handleColorByParameterChange(parameterIdentStr: string) { + setSelectedParameterIdentStr(parameterIdentStr); + } + function handleEnsembleSelectChange(ensembleIdentArr: EnsembleIdent[]) { setSelectedEnsembleIdents(ensembleIdentArr); } @@ -234,7 +286,6 @@ export function settings({ moduleContext, workbenchSession }: ModuleFCProps + + { + setColorRealizationsByParameter(event.target.checked); + }} + /> +
+ { + return { + value: elm.toString(), + label: elm.groupName ? `${elm.groupName}:${elm.name}` : elm.name, + }; + })} + value={selectedParameterIdentStr?.toString() ?? undefined} + onChange={handleColorByParameterChange} + /> +
+
[] { + xaxis = "x", + type = "scatter", +}: CreateVectorRealizationTraceOptions): Partial { // TODO: - // - type: "scattergl" or "scatter"? + // - type: "scattergl" or "scatter"? Maximum 8 WebGL contexts in Chrome gives issues? + // "scattergl" hides traces when zooming and panning for Ruben on work computer. // - vector name? // - realization number? // - lineShape - Each VectorRealizationData_api element has its own `is_rate` property. Should we // use that to determine the line shape or provide a lineShape argument? + return { + x: vectorRealizationData.timestamps_utc_ms, + y: vectorRealizationData.values, + line: { width: 1, color: color, shape: getLineShape(vectorRealizationData.is_rate) }, + mode: "lines", + type: type, + hovertemplate: `${hoverTemplate}Realization: ${vectorRealizationData.realization}, Ensemble: ${ensembleName}`, + // realizationNumber: realization.realization, + name: legendGroup, + legendgroup: legendGroup, + showlegend: vectorRealizationData.realization === 0 && showLegend ? true : false, + yaxis: yaxis, + xaxis: xaxis, + } as Partial; +} + +/** + Utility function for creating vector realization traces for an array of vector realization data + for given vector. + */ +export type CreateVectorRealizationTracesOptions = CreateRealizationTraceBaseOptions & { + vectorRealizationsData: VectorRealizationData_api[]; +}; +export function createVectorRealizationTraces({ + vectorRealizationsData, + ensembleName, + color, + legendGroup, + hoverTemplate = "", + showLegend = false, + yaxis = "y", + xaxis = "x", + type = "scatter", +}: CreateVectorRealizationTracesOptions): Partial[] { + // TODO: + // - lineShape - Each VectorRealizationData_api element has its own `is_rate` property. Should we + // use that to determine the line shape or provide a lineShape argument? + return vectorRealizationsData.map((realization) => { - return { - x: realization.timestamps_utc_ms, - y: realization.values, - line: { width: 1, color: color, shape: getLineShape(realization.is_rate) }, - mode: "lines", - type: "scattergl", - hovertemplate: `${hoverTemplate}Realization: ${realization.realization}, Ensemble: ${ensembleName}`, - // realizationNumber: realization.realization, - name: legendGroup, - legendgroup: legendGroup, - showlegend: realization.realization === 0 && showLegend ? true : false, - yaxis: yaxis, - xaxis: xaxis, - } as Partial; + return createVectorRealizationTrace({ + vectorRealizationData: realization, + ensembleName, + color, + legendGroup, + hoverTemplate, + showLegend, + yaxis, + xaxis, + type, + }); }); } /** Utility function for creating trace for historical vector data */ -export function createHistoricalVectorTrace( - vectorHistoricalData: VectorHistoricalData_api, +export type CreateHistoricalVectorTraceOptions = { + vectorHistoricalData: VectorHistoricalData_api; + color?: string; + yaxis?: string; + xaxis?: string; + showLegend?: boolean; + type?: "scatter" | "scattergl"; + // lineShape?: "linear" | "spline" | "hv" | "vh" | "hvh" | "vhv"; + vectorName?: string; + legendRank?: number; +}; +export function createHistoricalVectorTrace({ + vectorHistoricalData, color = "black", yaxis = "y", xaxis = "x", showLegend = false, - // lineShape: "linear" | "spline" | "hv" | "vh" | "hvh" | "vhv", - vectorName?: string, - legendRank?: number -): Partial { + type = "scatter", + vectorName, + legendRank, +}: CreateHistoricalVectorTraceOptions): Partial { const hoverText = vectorName ? `History: ${vectorName}` : "History"; return { line: { shape: getLineShape(vectorHistoricalData.is_rate), color: color }, mode: "lines", - type: "scatter", + type: type, x: vectorHistoricalData.timestamps_utc_ms, y: vectorHistoricalData.values, hovertext: hoverText, @@ -99,16 +164,27 @@ export function createHistoricalVectorTrace( only one of the statistics in each pair is present in the data. I.e. P10/P90 is neglected if only P10 or P90 is presented in the data. Similarly, MIN/MAX is neglected if only MIN or MAX is presented in the data. */ -export function createVectorFanchartTraces( - vectorStatisticData: VectorStatisticData_api, - hexColor: string, - legendGroup: string, +export type CreateVectorFanchartTracesOptions = { + vectorStatisticData: VectorStatisticData_api; + hexColor: string; + legendGroup: string; + yaxis?: string; + // lineShape?: "vh" | "linear" | "spline" | "hv" | "hvh" | "vhv"; + hoverTemplate?: string; + showLegend?: boolean; + legendRank?: number; + type?: "scatter" | "scattergl"; +}; +export function createVectorFanchartTraces({ + vectorStatisticData, + hexColor, + legendGroup, yaxis = "y", - // lineShape: "vh" | "linear" | "spline" | "hv" | "hvh" | "vhv" = "linear", hoverTemplate = "(%{x}, %{y})
", showLegend = false, - legendRank?: number -): Partial[] { + type = "scatter", + legendRank, +}: CreateVectorFanchartTracesOptions): Partial[] { const lowData = vectorStatisticData.value_objects.find((v) => v.statistic_function === StatisticFunction_api.P90); const highData = vectorStatisticData.value_objects.find((v) => v.statistic_function === StatisticFunction_api.P10); let lowHighData: LowHighData | undefined = undefined; @@ -156,6 +232,7 @@ export function createVectorFanchartTraces( hoverTemplate: hoverTemplate, legendRank: legendRank, yaxis: yaxis, + type: type, }); } @@ -165,17 +242,29 @@ export function createVectorFanchartTraces( The function creates lines for P10, P50, P90, MIN, MAX, and MEAN. Solid line for MEAN, various dashed lines for the remaining statistics. */ -export function createVectorStatisticsTraces( - vectorStatisticData: VectorStatisticData_api, - color: string, - legendGroup: string, +export type CreateVectorStatisticsTracesOptions = { + vectorStatisticData: VectorStatisticData_api; + hexColor: string; + legendGroup: string; + yaxis?: string; + // lineShape?: "vh" | "linear" | "spline" | "hv" | "hvh" | "vhv"; + lineWidth?: number; + hoverTemplate?: string; + showLegend?: boolean; + legendRank?: number; + type?: "scatter" | "scattergl"; +}; +export function createVectorStatisticsTraces({ + vectorStatisticData, + hexColor, + legendGroup, yaxis = "y", - // lineShape: "vh" | "linear" | "spline" | "hv" | "hvh" | "vhv" = "linear", lineWidth = 2, hoverTemplate = "(%{x}, %{y})
", showLegend = false, - legendRank?: number -): Partial[] { + type = "scatter", + legendRank, +}: CreateVectorStatisticsTracesOptions): Partial[] { const lowValueObject = vectorStatisticData.value_objects.find( (v) => v.statistic_function === StatisticFunction_api.P90 ); @@ -220,7 +309,7 @@ export function createVectorStatisticsTraces( return createStatisticsTraces({ data: statisticsData, - color: color, + color: hexColor, legendGroup: legendGroup, lineShape: getLineShape(vectorStatisticData.is_rate), lineWidth: lineWidth, @@ -228,5 +317,6 @@ export function createVectorStatisticsTraces( hoverTemplate: hoverTemplate, legendRank: legendRank, yaxis: yaxis, + type: type, }); } diff --git a/frontend/src/modules/SimulationTimeSeriesMatrix/utils/PlotlyTraceUtils/fanchartPlotting.ts b/frontend/src/modules/SimulationTimeSeriesMatrix/utils/PlotlyTraceUtils/fanchartPlotting.ts index da29c6526..162a07894 100644 --- a/frontend/src/modules/SimulationTimeSeriesMatrix/utils/PlotlyTraceUtils/fanchartPlotting.ts +++ b/frontend/src/modules/SimulationTimeSeriesMatrix/utils/PlotlyTraceUtils/fanchartPlotting.ts @@ -126,6 +126,7 @@ export type CreateFanchartTracesOptions = { showHoverInfo?: boolean; hoverText?: string; legendName?: string; + type?: "scatter" | "scattergl"; // hovermode?: string, }; @@ -173,6 +174,7 @@ export function createFanchartTraces({ showHoverInfo = true, hoverText = "", legendName = undefined, + type = "scatter", }: CreateFanchartTracesOptions): Partial[] { // NOTE: // - hovermode? not exposed? @@ -198,7 +200,7 @@ export function createFanchartTraces({ xaxis: xaxis, yaxis: yaxis, mode: "lines", - type: "scatter", + type: type, line: { width: 0, color: lineColor, shape: lineShape }, legendgroup: legendGroup, showlegend: false, diff --git a/frontend/src/modules/SimulationTimeSeriesMatrix/utils/PlotlyTraceUtils/statisticsPlotting.ts b/frontend/src/modules/SimulationTimeSeriesMatrix/utils/PlotlyTraceUtils/statisticsPlotting.ts index 37befdcb3..698f83b00 100644 --- a/frontend/src/modules/SimulationTimeSeriesMatrix/utils/PlotlyTraceUtils/statisticsPlotting.ts +++ b/frontend/src/modules/SimulationTimeSeriesMatrix/utils/PlotlyTraceUtils/statisticsPlotting.ts @@ -106,6 +106,7 @@ export type CreateStatisticsTracesOptions = { showHoverInfo?: boolean; hoverText?: string; legendName?: string; + type?: "scatter" | "scattergl"; // hovermode?: string, }; @@ -142,6 +143,7 @@ export function createStatisticsTraces({ hoverText = "", hoverTemplate = undefined, legendRank = undefined, + type = "scatter", }: CreateStatisticsTracesOptions): Partial[] { // NOTE: // - hovermode? not exposed? @@ -156,7 +158,7 @@ export function createStatisticsTraces({ xaxis: xaxis, yaxis: yaxis, mode: "lines", - type: "scatter", + type: type, line: { color: color, width: lineWidth, shape: lineShape }, legendgroup: legendGroup, showlegend: false, diff --git a/frontend/src/modules/SimulationTimeSeriesMatrix/utils/ensemblesContinuousParameterColoring.ts b/frontend/src/modules/SimulationTimeSeriesMatrix/utils/ensemblesContinuousParameterColoring.ts new file mode 100644 index 000000000..137569aa8 --- /dev/null +++ b/frontend/src/modules/SimulationTimeSeriesMatrix/utils/ensemblesContinuousParameterColoring.ts @@ -0,0 +1,65 @@ +import { Ensemble } from "@framework/Ensemble"; +import { ContinuousParameter, ParameterIdent, ParameterType } from "@framework/EnsembleParameters"; +import { ColorScale } from "@lib/utils/ColorScale"; +import { MinMax } from "@lib/utils/MinMax"; + +export class EnsemblesContinuousParameterColoring { + /** + * Helper class working with coloring according to selected continuous parameter across multiple ensembles + * + * Retrieves min/max parameter value across all ensembles and provides interface for retrieving parameter + * value within the min/max range for specific realization in ensemble. + */ + + private _parameterIdent: ParameterIdent; + private _ensembleContinuousParameterSet: { [ensembleName: string]: ContinuousParameter }; + private _colorScale: ColorScale; + + constructor(selectedEnsembles: Ensemble[], parameterIdent: ParameterIdent, colorScale: ColorScale) { + this._parameterIdent = parameterIdent; + this._ensembleContinuousParameterSet = {}; + let minMax = MinMax.createInvalid(); + for (const ensemble of selectedEnsembles) { + const parameters = ensemble.getParameters(); + if (!parameters.hasParameter(parameterIdent)) continue; + + const parameter = parameters.getParameter(parameterIdent); + if (parameter.type === ParameterType.CONTINUOUS) { + this._ensembleContinuousParameterSet[ensemble.getEnsembleName()] = parameter; + minMax = minMax.extendedBy(parameters.getContinuousParameterMinMax(parameterIdent)); + } + } + + // Consider: Set Range [0,0] if parameterMinMax is invalid? + this._colorScale = colorScale; + const midValue = minMax.min + (minMax.max - minMax.min) / 2; + this._colorScale.setRangeAndMidPoint(minMax.min, minMax.max, midValue); + } + + getColorScale(): ColorScale { + return this._colorScale; + } + + hasEnsembleName(ensembleName: string): boolean { + return ensembleName in this._ensembleContinuousParameterSet; + } + + hasParameterRealizationNumericalValue(ensembleName: string, realization: number): boolean { + if (!this.hasEnsembleName(ensembleName)) return false; + + const parameter = this._ensembleContinuousParameterSet[ensembleName]; + return parameter.realizations.indexOf(realization) !== -1; + } + + getParameterRealizationValue(ensembleName: string, realization: number): number { + if (!this.hasParameterRealizationNumericalValue(ensembleName, realization)) { + throw new Error( + `Parameter ${this._parameterIdent.toString()} has no numerical value for realization ${realization} in ensemble ${ensembleName}` + ); + } + + const parameter = this._ensembleContinuousParameterSet[ensembleName]; + const realizationIndex = parameter.realizations.indexOf(realization); + return parameter.values[realizationIndex]; + } +} diff --git a/frontend/src/modules/SimulationTimeSeriesMatrix/utils/ensemblesVectorListHelper.ts b/frontend/src/modules/SimulationTimeSeriesMatrix/utils/ensemblesVectorListHelper.ts index 7db9cb2c4..c94644400 100644 --- a/frontend/src/modules/SimulationTimeSeriesMatrix/utils/ensemblesVectorListHelper.ts +++ b/frontend/src/modules/SimulationTimeSeriesMatrix/utils/ensemblesVectorListHelper.ts @@ -30,16 +30,16 @@ export class EnsembleVectorListsHelper { * * @returns Array of unique vector names, as union of all vectors in all queries */ - vectorsUnion(): VectorDescription_api[] { - const vectorUnion: VectorDescription_api[] = []; + vectorsUnion(): string[] { + const vectorUnion: string[] = []; for (const query of this._queries) { if (!query.data) continue; // Add vector if name is not already in vectorUnion for (const vector of query.data) { - if (!vectorUnion.some((v) => v.name === vector.name)) { - vectorUnion.push(vector); - } + if (vectorUnion.includes(vector.name)) continue; + + vectorUnion.push(vector.name); } } return vectorUnion; diff --git a/frontend/src/modules/SimulationTimeSeriesMatrix/utils/subplotBuilder.ts b/frontend/src/modules/SimulationTimeSeriesMatrix/utils/subplotBuilder.ts index e3cd6d986..303bd8b20 100644 --- a/frontend/src/modules/SimulationTimeSeriesMatrix/utils/subplotBuilder.ts +++ b/frontend/src/modules/SimulationTimeSeriesMatrix/utils/subplotBuilder.ts @@ -1,16 +1,19 @@ import { VectorHistoricalData_api, VectorRealizationData_api, VectorStatisticData_api } from "@api"; import { ColorSet } from "@lib/utils/ColorSet"; +import { PlotMarker } from "plotly.js"; // import { filterBrightness, formatHex, parseHex } from "culori"; import { Annotations, Layout } from "plotly.js"; import { createHistoricalVectorTrace, createVectorFanchartTraces, + createVectorRealizationTrace, createVectorRealizationTraces, createVectorStatisticsTraces, } from "./PlotlyTraceUtils/createVectorTracesUtils"; import { scaleHexColorLightness } from "./colorUtils"; +import { EnsemblesContinuousParameterColoring } from "./ensemblesContinuousParameterColoring"; import { TimeSeriesPlotData } from "./timeSeriesPlotData"; import { VectorSpec } from "../state"; @@ -43,20 +46,30 @@ export class SubplotBuilder { private _vectorHexColors: HexColorMap = {}; // private _brighten = filterBrightness(1.3, "rgb"); + private _hasRealizationsTraces = false; + private _hasRealizationsTracesColoredByParameter = false; private _hasHistoryTraces = false; private _hasObservationTraces = false; + private _historyVectorColor = "black"; private _observationColor = "black"; private _width = 0; private _height = 0; + private _scatterType: "scatter" | "scattergl"; + + private _ensemblesParameterColoring: EnsemblesContinuousParameterColoring | null = null; + private _parameterFallbackColor = "#808080"; + constructor( subplotOwner: SubplotOwner, selectedVectorSpecifications: VectorSpec[], colorSet: ColorSet, width: number, - height: number + height: number, + ensemblesParameterColoring?: EnsemblesContinuousParameterColoring, + scatterType: "scatter" | "scattergl" = "scatter" ) { this._selectedVectorSpecifications = selectedVectorSpecifications; this._width = width; @@ -83,6 +96,9 @@ export class SubplotBuilder { ? this._uniqueVectorNames.length : this._uniqueEnsembleNames.length; + this._ensemblesParameterColoring = ensemblesParameterColoring ?? null; + this._scatterType = scatterType; + // TODO: // - Handle keep uirevision? // - Assign same color to vector independent of order in vector list? @@ -145,31 +161,34 @@ export class SubplotBuilder { createGraphLegends(): void { let currentLegendRank = 1; - // Helper function to create legend trace - const subplotDataLegendTrace = (name: string, hexColor: string): Partial => { - return { - name: name, - x: [null], - y: [null], - legendgroup: name, - showlegend: true, - visible: true, - mode: "lines", - line: { color: hexColor }, - legendrank: currentLegendRank++, - yaxis: `y1`, + // Add legend for each vector/ensemble when not coloring by parameter + if (this._hasRealizationsTraces) { + // Helper function to create legend trace + const subplotDataLegendTrace = (name: string, hexColor: string): Partial => { + return { + name: name, + x: [null], + y: [null], + legendgroup: name, + showlegend: true, + visible: true, + mode: "lines", + line: { color: hexColor }, + legendrank: currentLegendRank++, + yaxis: `y1`, + }; }; - }; - // Add legend for each vector/ensemble on top - if (this._subplotOwner === SubplotOwner.ENSEMBLE) { - this._addedVectorsLegendTracker.forEach((vectorName) => { - this._plotData.push(subplotDataLegendTrace(vectorName, this._vectorHexColors[vectorName])); - }); - } else if (this._subplotOwner === SubplotOwner.VECTOR) { - this._addedEnsemblesLegendTracker.forEach((ensembleName) => { - this._plotData.push(subplotDataLegendTrace(ensembleName, this._ensembleHexColors[ensembleName])); - }); + // Add legend for each vector/ensemble on top + if (this._subplotOwner === SubplotOwner.ENSEMBLE) { + this._addedVectorsLegendTracker.forEach((vectorName) => { + this._plotData.push(subplotDataLegendTrace(vectorName, this._vectorHexColors[vectorName])); + }); + } else if (this._subplotOwner === SubplotOwner.VECTOR) { + this._addedEnsemblesLegendTracker.forEach((ensembleName) => { + this._plotData.push(subplotDataLegendTrace(ensembleName, this._ensembleHexColors[ensembleName])); + }); + } } // Add legend for history trace with legendrank after vectors/ensembles @@ -208,6 +227,82 @@ export class SubplotBuilder { this._plotData.push(observationLegendTrace); } + + // Add color scale for color by parameter below the legends + if (this._hasRealizationsTracesColoredByParameter && this._ensemblesParameterColoring !== null) { + const colorScaleMarker: Partial = { + ...this._ensemblesParameterColoring.getColorScale().getAsPlotlyColorScaleMarkerObject(), + colorbar: { + title: "Parameter range", + titleside: "right", + ticks: "outside", + len: 0.75, // Note: If too many legends are added, this len might have to be reduced? + }, + }; + const parameterColorLegendTrace: Partial = { + x: [null], + y: [null], + marker: colorScaleMarker, + showlegend: false, + }; + this._plotData.push(parameterColorLegendTrace); + } + } + + addRealizationTracesColoredByParameter( + vectorsRealizationData: { vectorSpecification: VectorSpec; data: VectorRealizationData_api[] }[] + ): void { + if (this._ensemblesParameterColoring === null) return; + + // Only allow selected vectors + const selectedVectorsRealizationData = vectorsRealizationData.filter((vec) => + this._selectedVectorSpecifications.some( + (selectedVec) => selectedVec.vectorName === vec.vectorSpecification.vectorName + ) + ); + + const addLegendForTraces = false; + const hoverTemplate = ""; // No template yet + + // Create traces for each vector + for (const elm of selectedVectorsRealizationData) { + const subplotIndex = this.getSubplotIndex(elm.vectorSpecification); + if (subplotIndex === -1) continue; + + // Get legend group and color + const legendGroup = this.getLegendGroupAndUpdateTracker(elm.vectorSpecification); + + // Add traces for each realization with color based on parameter value + for (const realizationData of elm.data) { + let parameterColor = this._parameterFallbackColor; + const ensembleName = elm.vectorSpecification.ensembleIdent.getEnsembleName(); + if ( + this._ensemblesParameterColoring.hasParameterRealizationNumericalValue( + ensembleName, + realizationData.realization + ) + ) { + const value = this._ensemblesParameterColoring.getParameterRealizationValue( + ensembleName, + realizationData.realization + ); + parameterColor = this._ensemblesParameterColoring.getColorScale().getColorForValue(value); + } + + const vectorRealizationTrace = createVectorRealizationTrace({ + vectorRealizationData: realizationData, + ensembleName: elm.vectorSpecification.ensembleIdent.getEnsembleName(), + color: parameterColor, + legendGroup: legendGroup, + hoverTemplate: hoverTemplate, + showLegend: addLegendForTraces, + yaxis: `y${subplotIndex + 1}`, + type: this._scatterType, + }); + this._plotData.push(vectorRealizationTrace); + this._hasRealizationsTracesColoredByParameter = true; + } + } } addRealizationsTraces( @@ -225,9 +320,9 @@ export class SubplotBuilder { const hoverTemplate = ""; // No template yet // Create traces for each vector - selectedVectorsRealizationData.forEach((elm) => { + for (const elm of selectedVectorsRealizationData) { const subplotIndex = this.getSubplotIndex(elm.vectorSpecification); - if (subplotIndex === -1) return; + if (subplotIndex === -1) continue; // Get legend group and color const legendGroup = this.getLegendGroupAndUpdateTracker(elm.vectorSpecification); @@ -244,18 +339,20 @@ export class SubplotBuilder { color = scaleHexColorLightness(color, 1.3) ?? color; } - const vectorRealizationTraces = createVectorRealizationTraces( - elm.data, - elm.vectorSpecification.ensembleIdent.getEnsembleName(), - color, - legendGroup, - hoverTemplate, - addLegendForTraces, - `y${subplotIndex + 1}` - ); + const vectorRealizationTraces = createVectorRealizationTraces({ + vectorRealizationsData: elm.data, + ensembleName: elm.vectorSpecification.ensembleIdent.getEnsembleName(), + color: color, + legendGroup: legendGroup, + hoverTemplate: hoverTemplate, + showLegend: addLegendForTraces, + yaxis: `y${subplotIndex + 1}`, + type: this._scatterType, + }); this._plotData.push(...vectorRealizationTraces); - }); + this._hasRealizationsTraces = true; + } } addFanchartTraces( @@ -269,23 +366,24 @@ export class SubplotBuilder { ); // Create traces for each vector - selectedVectorsStatisticData.forEach((elm) => { + for (const elm of selectedVectorsStatisticData) { const subplotIndex = this.getSubplotIndex(elm.vectorSpecification); - if (subplotIndex === -1) return; + if (subplotIndex === -1) continue; // Get legend group and color const legendGroup = this.getLegendGroupAndUpdateTracker(elm.vectorSpecification); const color = this.getHexColor(elm.vectorSpecification); - const vectorFanchartTraces = createVectorFanchartTraces( - elm.data, - color, - legendGroup, - `y${subplotIndex + 1}` - ); + const vectorFanchartTraces = createVectorFanchartTraces({ + vectorStatisticData: elm.data, + hexColor: color, + legendGroup: legendGroup, + yaxis: `y${subplotIndex + 1}`, + type: this._scatterType, + }); this._plotData.push(...vectorFanchartTraces); - }); + } } addStatisticsTraces( @@ -302,24 +400,25 @@ export class SubplotBuilder { const lineWidth = highlightStatisticTraces ? 3 : 2; // Create traces for each vector - selectedVectorsStatisticData.forEach((elm) => { + for (const elm of selectedVectorsStatisticData) { const subplotIndex = this.getSubplotIndex(elm.vectorSpecification); - if (subplotIndex === -1) return; + if (subplotIndex === -1) continue; // Get legend group and color const legendGroup = this.getLegendGroupAndUpdateTracker(elm.vectorSpecification); const color = this.getHexColor(elm.vectorSpecification); - const vectorStatisticsTraces = createVectorStatisticsTraces( - elm.data, - color, - legendGroup, - `y${subplotIndex + 1}`, - lineWidth - ); + const vectorStatisticsTraces = createVectorStatisticsTraces({ + vectorStatisticData: elm.data, + hexColor: color, + legendGroup: legendGroup, + yaxis: `y${subplotIndex + 1}`, + lineWidth: lineWidth, + type: this._scatterType, + }); this._plotData.push(...vectorStatisticsTraces); - }); + } } addHistoryTraces( @@ -336,18 +435,19 @@ export class SubplotBuilder { ); // Create traces for each vector - selectedVectorsHistoricalData.forEach((elm) => { + for (const elm of selectedVectorsHistoricalData) { const subplotIndex = this.getSubplotIndex(elm.vectorSpecification); - if (subplotIndex === -1) return; + if (subplotIndex === -1) continue; this._hasHistoryTraces = true; - const vectorHistoryTrace = createHistoricalVectorTrace( - elm.data, - this._historyVectorColor, - `y${subplotIndex + 1}` - ); + const vectorHistoryTrace = createHistoricalVectorTrace({ + vectorHistoricalData: elm.data, + color: this._historyVectorColor, + yaxis: `y${subplotIndex + 1}`, + type: this._scatterType, + }); this._plotData.push(vectorHistoryTrace); - }); + } } addVectorObservations(): void { diff --git a/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx b/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx index 0b4bc4be9..deb6a8920 100644 --- a/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx +++ b/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx @@ -1,13 +1,15 @@ import React from "react"; import Plot from "react-plotly.js"; +import { Ensemble } from "@framework/Ensemble"; import { ModuleFCProps } from "@framework/Module"; +import { useEnsembleSet } from "@framework/WorkbenchSession"; import { useElementSize } from "@lib/hooks/useElementSize"; -// Note: Have for debug render count info -import { isDevMode } from "@lib/utils/devMode"; +import { ColorScaleGradientType } from "@lib/utils/ColorScale"; import { useHistoricalVectorDataQueries, useStatisticalVectorDataQueries, useVectorDataQueries } from "./queryHooks"; import { GroupBy, State, VisualizationMode } from "./state"; +import { EnsemblesContinuousParameterColoring } from "./utils/ensemblesContinuousParameterColoring"; import { SubplotBuilder, SubplotOwner } from "./utils/subplotBuilder"; import { createLoadedVectorSpecificationAndDataArray, @@ -15,19 +17,13 @@ import { filterVectorSpecificationAndIndividualStatisticsDataArray, } from "./utils/vectorSpecificationsAndQueriesUtils"; -export const view = ({ moduleContext, workbenchSettings }: ModuleFCProps) => { - // Leave this in until we get a feeling for React18/Plotly - const renderCount = React.useRef(0); - React.useEffect(function incrementRenderCount() { - renderCount.current = renderCount.current + 1; - }); - +export const view = ({ moduleContext, workbenchSession, workbenchSettings }: ModuleFCProps) => { const wrapperDivRef = React.useRef(null); const wrapperDivSize = useElementSize(wrapperDivRef); - const colorSet = workbenchSettings.useColorSet(); + const ensembleSet = useEnsembleSet(workbenchSession); - // State + // Store values const vectorSpecifications = moduleContext.useStoreValue("vectorSpecifications"); const groupBy = moduleContext.useStoreValue("groupBy"); const resampleFrequency = moduleContext.useStoreValue("resamplingFrequency"); @@ -35,6 +31,14 @@ export const view = ({ moduleContext, workbenchSettings }: ModuleFCProps) const visualizationMode = moduleContext.useStoreValue("visualizationMode"); const showHistorical = moduleContext.useStoreValue("showHistorical"); const statisticsSelection = moduleContext.useStoreValue("statisticsSelection"); + const parameterIdent = moduleContext.useStoreValue("parameterIdent"); + const colorRealizationsByParameter = moduleContext.useStoreValue("colorRealizationsByParameter"); + + // Color palettes + const colorSet = workbenchSettings.useColorSet(); + const parameterColorScale = workbenchSettings.useContinuousColorScale({ + gradientType: ColorScaleGradientType.Diverging, + }); // Queries const vectorDataQueries = useVectorDataQueries( @@ -60,6 +64,9 @@ export const view = ({ moduleContext, workbenchSettings }: ModuleFCProps) ); // Map vector specifications and queries with data + // TODO: + // - Add loading state if 1 or more queries are loading? + // - Can check for equal length of useQueries arrays and the loadedVectorSpecificationsAndData arrays? const loadedVectorSpecificationsAndRealizationData = vectorSpecifications ? createLoadedVectorSpecificationAndDataArray(vectorSpecifications, vectorDataQueries) : []; @@ -73,33 +80,50 @@ export const view = ({ moduleContext, workbenchSettings }: ModuleFCProps) ) : []; - // TODO: - // - Add loading state if 1 or more queries are loading? - // - Can check for equal length of useQueries arrays and the loadedVectorSpecificationsAndData arrays? + // Retrieve selected ensembles from vector specifications + const selectedEnsembles: Ensemble[] = []; + vectorSpecifications?.forEach((vectorSpecification) => { + if (selectedEnsembles.some((ensemble) => ensemble.getIdent().equals(vectorSpecification.ensembleIdent))) { + return; + } - // Iterate over unique ensemble names and assign color from color palette - if (vectorSpecifications) { - const uniqueEnsembleNames: string[] = []; - vectorSpecifications.forEach((vectorSpec) => { - const ensembleName = vectorSpec.ensembleIdent.getEnsembleName(); - if (!uniqueEnsembleNames.includes(ensembleName)) { - uniqueEnsembleNames.push(vectorSpec.ensembleIdent.getEnsembleName()); - } - }); - } + const ensemble = ensembleSet.findEnsemble(vectorSpecification.ensembleIdent); + if (ensemble === null) return; - // Plot builder - // NOTE: useRef? + selectedEnsembles.push(ensemble); + }); + + // Create parameter color scale helper + const doColorByParameter = + colorRealizationsByParameter && + parameterIdent !== null && + selectedEnsembles.some((ensemble) => ensemble.getParameters().findParameter(parameterIdent)); + const ensemblesParameterColoring = doColorByParameter + ? new EnsemblesContinuousParameterColoring(selectedEnsembles, parameterIdent, parameterColorScale) + : null; + + // Create Plot Builder const subplotOwner = groupBy === GroupBy.TIME_SERIES ? SubplotOwner.VECTOR : SubplotOwner.ENSEMBLE; + const scatterType = + visualizationMode === VisualizationMode.INDIVIDUAL_REALIZATIONS || + visualizationMode === VisualizationMode.STATISTICS_AND_REALIZATIONS + ? "scattergl" + : "scatter"; const subplotBuilder = new SubplotBuilder( subplotOwner, vectorSpecifications ?? [], colorSet, wrapperDivSize.width, - wrapperDivSize.height + wrapperDivSize.height, + ensemblesParameterColoring ?? undefined, + scatterType ); - if (visualizationMode === VisualizationMode.INDIVIDUAL_REALIZATIONS) { + // Add traces based on visualization mode + if (doColorByParameter && visualizationMode === VisualizationMode.INDIVIDUAL_REALIZATIONS) { + subplotBuilder.addRealizationTracesColoredByParameter(loadedVectorSpecificationsAndRealizationData); + } + if (!doColorByParameter && visualizationMode === VisualizationMode.INDIVIDUAL_REALIZATIONS) { const useIncreasedBrightness = false; subplotBuilder.addRealizationsTraces(loadedVectorSpecificationsAndRealizationData, useIncreasedBrightness); } @@ -153,12 +177,6 @@ export const view = ({ moduleContext, workbenchSettings }: ModuleFCProps) onHover={handleHover} onUnhover={handleUnHover} /> - {isDevMode() && ( - <> -
(rc={renderCount.current})
-
Traces: {plotData.length}
- - )} ); };