diff --git a/frontend/src/modules/Sensitivity/sensitivityChart.tsx b/frontend/src/modules/Sensitivity/sensitivityChart.tsx index 47d733a7a..a3f2eb5f1 100644 --- a/frontend/src/modules/Sensitivity/sensitivityChart.tsx +++ b/frontend/src/modules/Sensitivity/sensitivityChart.tsx @@ -6,8 +6,13 @@ import { Layout, PlotData, PlotMouseEvent } from "plotly.js"; import { SensitivityResponse, SensitivityResponseDataset } from "./sensitivityResponseCalculator"; import { SelectedSensitivity } from "./state"; +export type SensitivityColors = { + sensitivityName: string; + color: string; +}; export type sensitivityChartProps = { sensitivityResponseDataset: SensitivityResponseDataset; + sensitivityColors: SensitivityColors[]; showLabels: boolean; hideZeroY: boolean; showRealizationPoints: boolean; @@ -102,16 +107,8 @@ const lowTrace = ( sensitivityResponses: SensitivityResponse[], showLabels: boolean, selectedBar: SelectedBar | null, - barColor = "e53935" + colors: string[] ): Partial => { - // const label = showLabels - // ? sensitivityResponses.map( - // (s) => - // `${numFormat(s.lowCaseReferenceDifference)} ${numFormat(s.lowCaseAverage)}
Case: ${ - // s.lowCaseName - // }` - // ) - // : []; const label = showLabels ? sensitivityResponses.map((s) => computeLowLabel(s)) : []; return { @@ -128,7 +125,7 @@ const lowTrace = ( name: TraceGroup.LOW, orientation: "h", marker: { - color: barColor, + color: colors, line: { width: 3, color: sensitivityResponses.map((s, idx) => @@ -146,7 +143,7 @@ const highTrace = ( sensitivityResponses: SensitivityResponse[], showLabels: boolean, selectedBar: SelectedBar | null, - barColor = "#00897b" + colors: string[] ): Partial => { // const label = showLabels // ? sensitivityResponses.map( @@ -172,7 +169,7 @@ const highTrace = ( name: TraceGroup.HIGH, orientation: "h", marker: { - color: barColor, + color: colors, line: { width: 3, color: sensitivityResponses.map((s, idx) => @@ -201,9 +198,11 @@ const sensitivityChart: React.FC = (props) => { (s) => s.lowCaseReferenceDifference !== 0 || s.highCaseReferenceDifference !== 0 ); } - - traces.push(lowTrace(filteredSensitivityResponses, showLabels, selectedBar)); - traces.push(highTrace(filteredSensitivityResponses, showLabels, selectedBar)); + const colors: string[] = filteredSensitivityResponses.map( + (s) => props.sensitivityColors.find((sc) => sc.sensitivityName === s.sensitivityName)?.color ?? "black" + ); + traces.push(lowTrace(filteredSensitivityResponses, showLabels, selectedBar, colors)); + traces.push(highTrace(filteredSensitivityResponses, showLabels, selectedBar, colors)); // if (showRealizationPoints) { // TODO: Add realization points diff --git a/frontend/src/modules/Sensitivity/view.tsx b/frontend/src/modules/Sensitivity/view.tsx index 1dd06118c..74ae61f27 100644 --- a/frontend/src/modules/Sensitivity/view.tsx +++ b/frontend/src/modules/Sensitivity/view.tsx @@ -7,12 +7,21 @@ import { useEnsembleSet } from "@framework/WorkbenchSession"; import { AdjustmentsHorizontalIcon, ChartBarIcon, TableCellsIcon } from "@heroicons/react/20/solid"; import { useElementSize } from "@lib/hooks/useElementSize"; -import SensitivityChart from "./sensitivityChart"; -import { EnsembleScalarResponse, SensitivityResponseCalculator } from "./sensitivityResponseCalculator"; +import SensitivityChart, { SensitivityColors } from "./sensitivityChart"; +import { + EnsembleScalarResponse, + SensitivityResponseCalculator, + SensitivityResponseDataset, +} from "./sensitivityResponseCalculator"; import SensitivityTable from "./sensitivityTable"; import { PlotType, State } from "./state"; -export const view = ({ moduleContext, workbenchSession, workbenchServices }: ModuleFCProps) => { +export const view = ({ + moduleContext, + workbenchSession, + workbenchSettings, + workbenchServices, +}: ModuleFCProps) => { // Leave this in until we get a feeling for React18/Plotly const renderCount = React.useRef(0); React.useEffect(function incrementRenderCount() { @@ -78,24 +87,26 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod return unsubscribeFunc; }, [responseChannel, ensembleSet]); - // Memoize the computation of sensitivity responses. Should we use useMemo? const sensitivities = channelEnsemble?.getSensitivities(); - const computedSensitivityResponseDataset = React.useMemo(() => { - if (sensitivities && channelResponseData) { - // How to handle errors? - try { - const sensitivityResponseCalculator = new SensitivityResponseCalculator( - sensitivities, - channelResponseData - ); - return sensitivityResponseCalculator.computeSensitivitiesForResponse(); - } catch (e) { - console.warn(e); - return null; - } + const colorSet = workbenchSettings.useColorSet(); + const sensitivityColors: SensitivityColors[] = + sensitivities + ?.getSensitivityNames() + .map((sensitivityName, index) => + index === 0 + ? { sensitivityName, color: colorSet.getFirstColor() } + : { sensitivityName, color: colorSet.getNextColor() } + ) ?? []; + let computedSensitivityResponseDataset: SensitivityResponseDataset | null = null; + if (sensitivities && channelResponseData) { + // How to handle errors? + try { + const sensitivityResponseCalculator = new SensitivityResponseCalculator(sensitivities, channelResponseData); + computedSensitivityResponseDataset = sensitivityResponseCalculator.computeSensitivitiesForResponse(); + } catch (e) { + console.warn(e); } - return null; - }, [sensitivities, channelResponseData]); + } let errMessage = ""; if (!computedSensitivityResponseDataset) { @@ -176,6 +187,7 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod {computedSensitivityResponseDataset && plotType === PlotType.TORNADO && ( ("SimulationTimeSeriesSensitivity", defaultState); diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/queryHooks.tsx b/frontend/src/modules/SimulationTimeSeriesSensitivity/queryHooks.tsx index c15a151f3..2b57f476e 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/queryHooks.tsx +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/queryHooks.tsx @@ -1,4 +1,9 @@ -import { Frequency_api, VectorDescription_api, VectorStatisticSensitivityData_api } from "@api"; +import { + Frequency_api, + VectorDescription_api, + VectorHistoricalData_api, + VectorStatisticSensitivityData_api, +} from "@api"; import { VectorRealizationData_api } from "@api"; import { apiService } from "@framework/ApiService"; import { UseQueryResult, useQuery } from "@tanstack/react-query"; @@ -6,7 +11,7 @@ import { UseQueryResult, useQuery } from "@tanstack/react-query"; const STALE_TIME = 60 * 1000; const CACHE_TIME = 60 * 1000; -export function useVectorsQuery( +export function useVectorListQuery( caseUuid: string | undefined, ensembleName: string | undefined ): UseQueryResult> { @@ -72,3 +77,25 @@ export function useStatisticalVectorSensitivityDataQuery( enabled: allowEnable && caseUuid && ensembleName && vectorName && resampleFrequency ? true : false, }); } + +export function useHistoricalVectorDataQuery( + caseUuid: string | undefined, + ensembleName: string | undefined, + vectorName: string | undefined, + resampleFrequency: Frequency_api | null, + allowEnable: boolean +): UseQueryResult { + return useQuery({ + queryKey: ["getHistoricalVectorData", caseUuid, ensembleName, vectorName, resampleFrequency], + queryFn: () => + apiService.timeseries.getHistoricalVectorData( + caseUuid ?? "", + ensembleName ?? "", + vectorName ?? "", + resampleFrequency ?? Frequency_api.MONTHLY + ), + staleTime: STALE_TIME, + cacheTime: CACHE_TIME, + enabled: allowEnable && caseUuid && ensembleName && vectorName && resampleFrequency ? true : false, + }); +} diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/settings.tsx b/frontend/src/modules/SimulationTimeSeriesSensitivity/settings.tsx index 729073884..6271905c2 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/settings.tsx +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/settings.tsx @@ -10,11 +10,15 @@ import { fixupEnsembleIdent, maybeAssignFirstSyncedEnsemble } from "@framework/u import { ApiStateWrapper } from "@lib/components/ApiStateWrapper"; import { Checkbox } from "@lib/components/Checkbox"; import { CircularProgress } from "@lib/components/CircularProgress"; +import { CollapsibleGroup } from "@lib/components/CollapsibleGroup"; import { Dropdown, DropdownOption } from "@lib/components/Dropdown"; import { Label } from "@lib/components/Label"; import { Select, SelectOption } from "@lib/components/Select"; +import { SmartNodeSelectorSelection, TreeDataNode } from "@lib/components/SmartNodeSelector"; +import { VectorSelector } from "@lib/components/VectorSelector"; +import { createVectorSelectorDataFromVectors } from "@lib/utils/vectorSelectorUtils"; -import { useVectorsQuery } from "./queryHooks"; +import { useVectorListQuery } from "./queryHooks"; import { State } from "./state"; //----------------------------------------------------------------------------------------------------------- @@ -25,12 +29,12 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: const ensembleSet = useEnsembleSet(workbenchSession); const [selectedEnsembleIdent, setSelectedEnsembleIdent] = React.useState(null); - const [selectedVectorName, setSelectedVectorName] = React.useState(""); - const [selectedSensitivity, setSelectedSensitivity] = moduleContext.useStoreState("selectedSensitivity"); + const [selectedVectorName, setSelectedVectorName] = React.useState(null); + const [selectedSensitivities, setSelectedSensitivities] = moduleContext.useStoreState("selectedSensitivities"); const [resampleFrequency, setResamplingFrequency] = moduleContext.useStoreState("resamplingFrequency"); const [showStatistics, setShowStatistics] = moduleContext.useStoreState("showStatistics"); const [showRealizations, setShowRealizations] = moduleContext.useStoreState("showRealizations"); - + const [showHistorical, setShowHistorical] = moduleContext.useStoreState("showHistorical"); const syncedSettingKeys = moduleContext.useSyncedSettingKeys(); const syncHelper = new SyncSettingsHelper(syncedSettingKeys, workbenchServices); const syncedValueEnsembles = syncHelper.useValue(SyncSettingKey.ENSEMBLE, "global.syncValue.ensembles"); @@ -39,35 +43,46 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: const candidateEnsembleIdent = maybeAssignFirstSyncedEnsemble(selectedEnsembleIdent, syncedValueEnsembles); const computedEnsembleIdent = fixupEnsembleIdent(candidateEnsembleIdent, ensembleSet); - const vectorsQuery = useVectorsQuery( + const vectorsListQuery = useVectorListQuery( computedEnsembleIdent?.getCaseUuid(), computedEnsembleIdent?.getEnsembleName() ); + const vectorNames = vectorsListQuery.data?.map((vec) => vec.name) ?? []; + const vectorSelectorData: TreeDataNode[] = createVectorSelectorDataFromVectors(vectorNames); + let candidateVectorName = selectedVectorName; if (syncedValueSummaryVector?.vectorName) { console.debug(`${myInstanceIdStr} -- syncing timeSeries to ${syncedValueSummaryVector.vectorName}`); candidateVectorName = syncedValueSummaryVector.vectorName; } - const computedVectorName = fixupVectorName(candidateVectorName, vectorsQuery.data); + const computedVectorName = fixupVectorName(candidateVectorName, vectorNames); - if (computedEnsembleIdent && !computedEnsembleIdent.equals(selectedEnsembleIdent)) { - setSelectedEnsembleIdent(computedEnsembleIdent); - } if (computedVectorName && computedVectorName !== selectedVectorName) { setSelectedVectorName(computedVectorName); } - const computedEnsemble = computedEnsembleIdent ? ensembleSet.findEnsemble(computedEnsembleIdent) : null; - const sensitivities = computedEnsemble?.getSensitivities(); + const hasHistorical = + vectorsListQuery.data?.some((vec) => vec.name === computedVectorName && vec.has_historical) ?? false; - React.useEffect(() => { - const sensitivityNames = computedEnsemble?.getSensitivities()?.getSensitivityNames(); - if (sensitivityNames && sensitivityNames.length > 0) { - if (!selectedSensitivity || !sensitivityNames.includes(selectedSensitivity)) { - setSelectedSensitivity(sensitivityNames[0]); + if (computedEnsembleIdent && !computedEnsembleIdent.equals(selectedEnsembleIdent)) { + setSelectedEnsembleIdent(computedEnsembleIdent); + } + + const computedEnsemble = computedEnsembleIdent ? ensembleSet.findEnsemble(computedEnsembleIdent) : null; + const sensitivityNames = computedEnsemble?.getSensitivities()?.getSensitivityNames() ?? []; + React.useEffect( + function setInitialSensitivities() { + if (!selectedSensitivities && sensitivityNames.length > 0) { + setSelectedSensitivities(sensitivityNames); } - } - }, [computedEnsemble]); + }, + [selectedEnsembleIdent] + ); + const sensitivityOptions: SelectOption[] = + sensitivityNames.map((name) => ({ + value: name, + label: name, + })) ?? []; React.useEffect( function propagateVectorSpecToView() { @@ -75,6 +90,7 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: moduleContext.getStateStore().setValue("vectorSpec", { ensembleIdent: computedEnsembleIdent, vectorName: computedVectorName, + hasHistorical, }); } else { moduleContext.getStateStore().setValue("vectorSpec", null); @@ -91,15 +107,6 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: } } - function handleVectorSelectionChange(selectedVecNames: string[]) { - console.debug("handleVectorSelectionChange()"); - const newName = selectedVecNames[0] ?? ""; - setSelectedVectorName(newName); - if (newName) { - syncHelper.publishValue(SyncSettingKey.TIME_SERIES, "global.syncValue.timeSeries", { vectorName: newName }); - } - } - function handleFrequencySelectionChange(newFreqStr: string) { console.debug(`handleFrequencySelectionChange() newFreqStr=${newFreqStr}`); let newFreq: Frequency_api | null = null; @@ -109,68 +116,73 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: console.debug(`handleFrequencySelectionChange() newFreqStr=${newFreqStr} newFreq=${newFreq}`); setResamplingFrequency(newFreq); } - if (!sensitivities?.getSensitivityArr()) { - return
This is not a sensitivity ensemble
; + function handleVectorSelectChange(selection: SmartNodeSelectorSelection) { + setSelectedVectorName(selection.selectedTags[0]); + } + function handleShowHistorical(event: React.ChangeEvent) { + setShowHistorical(event.target.checked); } - return ( <> - + } + errorComponent={"Could not load the vectors for selected ensembles"} > - - - setShowStatistics(e.target.checked)} - /> - setShowRealizations(e.target.checked)} - /> + ); } @@ -178,16 +190,16 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: //----------------------------------------------------------------------------------------------------------- //----------------------------------------------------------------------------------------------------------- -function fixupVectorName(currVectorName: string, vectorDescriptionsArr: VectorDescription_api[] | undefined): string { - if (!vectorDescriptionsArr || vectorDescriptionsArr.length === 0) { +function fixupVectorName(currVectorName: string | null, availableVectorNames: string[] | undefined): string { + if (!availableVectorNames || availableVectorNames.length === 0) { return ""; } - if (vectorDescriptionsArr.find((item) => item.name === currVectorName)) { + if (availableVectorNames.find((name) => name === currVectorName) && currVectorName) { return currVectorName; } - return vectorDescriptionsArr[0].name; + return availableVectorNames[0]; } function makeVectorOptionItems(vectorDescriptionsArr: VectorDescription_api[] | undefined): SelectOption[] { diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/chart.tsx b/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/chart.tsx index 072fb6d6d..9afc28097 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/chart.tsx +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/chart.tsx @@ -15,6 +15,7 @@ export type TimeSeriesChartProps = { traceDataArr: TimeSeriesPlotlyTrace[]; activeTimestampUtcMs?: number; hoveredTimestampUtcMs?: number; + uirevision?: string; onHover?: (hoverData: HoverInfo | null) => void; onClick?: (timestampUtcMs: number) => void; height?: number | 100; @@ -74,6 +75,7 @@ export const TimeSeriesChart: React.FC = (props) => { legend: { orientation: "h", yanchor: "bottom", y: 1.02, xanchor: "right", x: 1 }, margin: { t: 0, b: 100, r: 0 }, shapes: [], + uirevision: props.uirevision, }; if (props.activeTimestampUtcMs !== undefined) { diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/traces.ts b/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/traces.ts index 79485833d..90229ebb1 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/traces.ts +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/traces.ts @@ -1,5 +1,8 @@ -import { VectorRealizationData_api } from "@api"; +import { VectorRealizationData_api, VectorStatisticSensitivityData_api } from "@api"; +import { StatisticFunction_api } from "@api"; +import { Sensitivity } from "@framework/EnsembleSensitivities"; +import { StringIterator } from "lodash"; import { PlotData } from "plotly.js"; export interface TimeSeriesPlotlyTrace extends Partial { @@ -7,18 +10,59 @@ export interface TimeSeriesPlotlyTrace extends Partial { legendrank?: number; } +export function createStatisticalLineTraces( + sensitivityData: VectorStatisticSensitivityData_api[], + color: string +): TimeSeriesPlotlyTrace[] { + const traces: TimeSeriesPlotlyTrace[] = []; + sensitivityData.forEach((aCase, index) => { + const meanObj = aCase.value_objects.find((obj) => obj.statistic_function === StatisticFunction_api.MEAN); + if (meanObj) { + traces.push( + createLineTrace({ + timestampsMsUtc: aCase.timestamps_utc_ms, + values: meanObj.values, + name: `${aCase.sensitivity_name}`, + legendGroup: `${aCase.sensitivity_name}`, + lineShape: "linear", + lineDash: "dash", + showLegend: index === 0, + lineColor: color, + lineWidth: 3, + hoverTemplate: `Sensitivity:${aCase.sensitivity_name}
Case: ${aCase.sensitivity_case}
Value: %{y}
Date: %{x}`, + }) + ); + } + }); + return traces; +} + export function createRealizationLineTraces( realizationData: VectorRealizationData_api[], + sensitivity: Sensitivity, + color: string, highlightedRealization?: number | undefined ): TimeSeriesPlotlyTrace[] { const traces: TimeSeriesPlotlyTrace[] = []; let highlightedTrace: TimeSeriesPlotlyTrace | null = null; realizationData.forEach((vec) => { - const curveColor = highlightedRealization === vec.realization ? "red" : "grey"; + const curveColor = highlightedRealization === vec.realization ? "red" : color; const lineWidth = highlightedRealization === vec.realization ? 2 : 1; const lineShape = highlightedRealization === vec.realization ? "spline" : "linear"; const isHighlighted = vec.realization === highlightedRealization ? true : false; - const trace = createSingleRealizationLineTrace(vec, curveColor, lineWidth, lineShape); + + const trace = createLineTrace({ + timestampsMsUtc: vec.timestamps_utc_ms, + values: vec.values, + name: `real-${vec.realization}`, + lineShape: lineShape, + lineDash: "solid", + showLegend: false, + lineColor: curveColor, + lineWidth: lineWidth, + hoverTemplate: `Sensitivity:${sensitivity.name}
Value: %{y}
Date: %{x}`, + }); + if (isHighlighted) { highlightedTrace = trace; } else { @@ -30,6 +74,31 @@ export function createRealizationLineTraces( } return traces; } +export type LineTraceData = { + timestampsMsUtc: number[]; + values: number[]; + name: string; + hoverTemplate?: string; + legendGroup?: string; + lineShape: "linear" | "spline"; + lineDash: "dash" | "dot" | "dashdot" | "solid"; + showLegend: boolean; + lineColor: string; + lineWidth: number; +}; +export function createLineTrace(data: LineTraceData): TimeSeriesPlotlyTrace { + return { + x: data.timestampsMsUtc, + y: data.values, + name: data.name, + showlegend: data.showLegend, + hovertemplate: data.hoverTemplate, + legendgroup: data.legendGroup, + type: "scatter", + mode: "lines", + line: { color: data.lineColor, width: data.lineWidth, dash: data.lineDash, shape: data.lineShape }, + }; +} function createSingleRealizationLineTrace( vec: VectorRealizationData_api, @@ -62,7 +131,7 @@ export function createSensitivityStatisticsTrace( x: timestampsMsUtc, y: values, name: name, - legendrank: -1, + showlegend: false, type: "scatter", mode: "lines", line: { color: lineColor || "lightblue", width: 4, dash: lineDash, shape: lineShape }, diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/state.ts b/frontend/src/modules/SimulationTimeSeriesSensitivity/state.ts index 8a97034c9..79256418a 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/state.ts +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/state.ts @@ -4,13 +4,15 @@ import { EnsembleIdent } from "@framework/EnsembleIdent"; export interface VectorSpec { ensembleIdent: EnsembleIdent; vectorName: string; + hasHistorical: boolean; } export interface State { vectorSpec: VectorSpec | null; resamplingFrequency: Frequency_api | null; - selectedSensitivity: string | null; + selectedSensitivities: string[] | null; showStatistics: boolean; showRealizations: boolean; realizationsToInclude: number[] | null; + showHistorical: boolean; } diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/view.tsx b/frontend/src/modules/SimulationTimeSeriesSensitivity/view.tsx index a4e965a38..3f2a46d1f 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/view.tsx +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/view.tsx @@ -1,7 +1,12 @@ import React from "react"; -import { StatisticFunction_api, VectorRealizationData_api, VectorStatisticSensitivityData_api } from "@api"; -import { BroadcastChannelMeta, BroadcastChannelData } from "@framework/Broadcaster"; +import { + StatisticFunction_api, + VectorHistoricalData_api, + VectorRealizationData_api, + VectorStatisticSensitivityData_api, +} from "@api"; +import { BroadcastChannelData, BroadcastChannelMeta } from "@framework/Broadcaster"; import { Ensemble } from "@framework/Ensemble"; import { ModuleFCProps } from "@framework/Module"; import { useSubscribedValue } from "@framework/WorkbenchServices"; @@ -11,13 +16,27 @@ import { useElementSize } from "@lib/hooks/useElementSize"; import { indexOf } from "lodash"; import { BroadcastChannelNames } from "./channelDefs"; -import { useStatisticalVectorSensitivityDataQuery, useVectorDataQuery } from "./queryHooks"; +import { + useHistoricalVectorDataQuery, + useStatisticalVectorSensitivityDataQuery, + useVectorDataQuery, +} from "./queryHooks"; import { HoverInfo, TimeSeriesChart } from "./simulationTimeSeriesChart/chart"; -import { TimeSeriesPlotlyTrace } from "./simulationTimeSeriesChart/traces"; -import { createRealizationLineTraces, createSensitivityStatisticsTrace } from "./simulationTimeSeriesChart/traces"; +import { TimeSeriesPlotlyTrace, createStatisticalLineTraces } from "./simulationTimeSeriesChart/traces"; +import { + LineTraceData, + createLineTrace, + createRealizationLineTraces, + createSensitivityStatisticsTrace, +} from "./simulationTimeSeriesChart/traces"; import { State } from "./state"; -export const view = ({ moduleContext, workbenchSession, workbenchServices }: ModuleFCProps) => { +export const view = ({ + moduleContext, + workbenchSession, + workbenchSettings, + workbenchServices, +}: ModuleFCProps) => { // Leave this in until we get a feeling for React18/Plotly const renderCount = React.useRef(0); React.useEffect(function incrementRenderCount() { @@ -30,7 +49,7 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod const resampleFrequency = moduleContext.useStoreValue("resamplingFrequency"); const showStatistics = moduleContext.useStoreValue("showStatistics"); const showRealizations = moduleContext.useStoreValue("showRealizations"); - const selectedSensitivity = moduleContext.useStoreValue("selectedSensitivity"); + const selectedSensitivities = moduleContext.useStoreValue("selectedSensitivities"); const [activeTimestampUtcMs, setActiveTimestampUtcMs] = React.useState(null); const subscribedHoverTimestampUtcMs = useSubscribedValue("global.hoverTimestamp", workbenchServices); @@ -50,6 +69,13 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod resampleFrequency, showStatistics ); + const historicalQuery = useHistoricalVectorDataQuery( + vectorSpec?.ensembleIdent.getCaseUuid(), + vectorSpec?.ensembleIdent.getEnsembleName(), + vectorSpec?.vectorName, + resampleFrequency, + vectorSpec?.hasHistorical || false + ); const ensembleSet = workbenchSession.getEnsembleSet(); const ensemble = vectorSpec ? ensembleSet.findEnsemble(vectorSpec.ensembleIdent) : null; @@ -82,20 +108,56 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod }, [ensemble, vectorSpec, realizationsQuery.data, activeTimestampUtcMs, moduleContext] ); + const colorSet = workbenchSettings.useColorSet(); - const traceDataArr = React.useMemo(() => { - if (!ensemble || !selectedSensitivity) { - return []; + const allSensitivityNamesInEnsemble = ensemble?.getSensitivities()?.getSensitivityNames().sort() ?? []; + const traceDataArr: TimeSeriesPlotlyTrace[] = []; + if (ensemble && selectedSensitivities && selectedSensitivities.length > 0) { + // Loop through all available sensitivities to get correct color + allSensitivityNamesInEnsemble.forEach((sensitivityName, index) => { + const color = index === 0 ? colorSet.getFirstColor() : colorSet.getNextColor(); + + // Work on selected sensitivities only + if (selectedSensitivities.includes(sensitivityName)) { + // Add statistics traces + if (showStatistics && statisticsQuery.data) { + const matchingCases: VectorStatisticSensitivityData_api[] = statisticsQuery.data.filter( + (stat) => stat.sensitivity_name === sensitivityName + ); + const traces = createStatisticalLineTraces(matchingCases, color); + traceDataArr.push(...traces); + } + + // Add realization traces + const sensitivity = ensemble.getSensitivities()?.getSensitivityByName(sensitivityName); + if (showRealizations && realizationsQuery.data && sensitivity) { + for (const sensCase of sensitivity.cases) { + const realsToInclude = sensCase.realizations; + const realizationData: VectorRealizationData_api[] = realizationsQuery.data.filter((vec) => + realsToInclude.includes(vec.realization) + ); + const traces = createRealizationLineTraces(realizationData, sensitivity, color); + traceDataArr.push(...traces); + } + } + } + }); + // Add history + if (historicalQuery?.data) { + traceDataArr.push( + createLineTrace({ + timestampsMsUtc: historicalQuery.data.timestamps_utc_ms, + values: historicalQuery.data.values, + name: "history", + lineShape: "linear", + lineDash: "solid", + showLegend: true, + lineColor: "black", + lineWidth: 2, + }) + ); } - return buildTraceDataArr( - ensemble, - selectedSensitivity, - showStatistics, - showRealizations, - statisticsQuery.data, - realizationsQuery.data - ); - }, [ensemble, selectedSensitivity, showStatistics, showRealizations, statisticsQuery.data, realizationsQuery.data]); + } function handleHoverInChart(hoverInfo: HoverInfo | null) { if (hoverInfo) { @@ -126,6 +188,7 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod
); }; - -function buildTraceDataArr( - ensemble: Ensemble, - sensitivityName: string, - showStatistics: boolean, - showRealizations: boolean, - perSensitivityStatisticData?: VectorStatisticSensitivityData_api[], - perRealizationData?: VectorRealizationData_api[] -): TimeSeriesPlotlyTrace[] { - const sensitivity = ensemble.getSensitivities()?.getSensitivityByName(sensitivityName); - if (!sensitivity) { - return []; - } - - const traceDataArr: TimeSeriesPlotlyTrace[] = []; - - if (perSensitivityStatisticData) { - const refCase = perSensitivityStatisticData.find((stat) => stat.sensitivity_name === "rms_seed"); - const meanObj = refCase?.value_objects.find((obj) => obj.statistic_function === StatisticFunction_api.MEAN); - if (refCase && meanObj) { - traceDataArr.push( - createSensitivityStatisticsTrace( - refCase.timestamps_utc_ms, - meanObj.values, - `reference ${refCase.sensitivity_name}`, - "linear", - "solid", - "black" - ) - ); - } - } - - if (showStatistics && perSensitivityStatisticData) { - const matchingCases = perSensitivityStatisticData.filter((stat) => stat.sensitivity_name === sensitivityName); - for (const aCase of matchingCases) { - const meanObj = aCase.value_objects.find((obj) => obj.statistic_function === StatisticFunction_api.MEAN); - if (meanObj) { - traceDataArr.push( - createSensitivityStatisticsTrace( - aCase.timestamps_utc_ms, - meanObj.values, - aCase.sensitivity_case, - "linear", - "dash" - ) - ); - } - } - } - - if (showRealizations && perRealizationData) { - for (const sensCase of sensitivity.cases) { - const realsToInclude = sensCase.realizations; - const realizationData: VectorRealizationData_api[] = perRealizationData.filter((vec) => - realsToInclude.includes(vec.realization) - ); - const traces = createRealizationLineTraces(realizationData); - traceDataArr.push(...traces); - } - } - - return traceDataArr; -}