diff --git a/frontend/src/modules/Sensitivity/queryHooks.tsx b/frontend/src/modules/Sensitivity/queryHooks.tsx deleted file mode 100644 index bc5bd6899..000000000 --- a/frontend/src/modules/Sensitivity/queryHooks.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { EnsembleScalarResponse_api } from "@api"; -import { apiService } from "@framework/ApiService"; -import { UseQueryResult, useQuery } from "@tanstack/react-query"; - -const STALE_TIME = 60 * 1000; -const CACHE_TIME = 60 * 1000; - -export function useInplaceResponseQuery( - caseUuid: string | null, - ensembleName: string | null, - tableName: string | null, - responseName: string | null - // requestBody: Body_get_realizations_response | null, -): UseQueryResult { - return useQuery({ - queryKey: ["getRealizationResponse", caseUuid, ensembleName, tableName, responseName], //, requestBody], - queryFn: () => - apiService.inplaceVolumetrics.getRealizationsResponse( - caseUuid ?? "", - ensembleName ?? "", - tableName ?? "", - responseName ?? "" - // requestBody ?? {} - ), - staleTime: STALE_TIME, - cacheTime: CACHE_TIME, - enabled: caseUuid && ensembleName && tableName && responseName ? true : false, - }); -} diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/loadModule.tsx b/frontend/src/modules/SimulationTimeSeriesSensitivity/loadModule.tsx index c77fa44ee..4d5778621 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/loadModule.tsx +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/loadModule.tsx @@ -8,10 +8,11 @@ import { view } from "./view"; const defaultState: State = { vectorSpec: null, resamplingFrequency: Frequency_api.MONTHLY, - selectedSensitivity: null, + selectedSensitivities: null, showStatistics: true, showRealizations: false, realizationsToInclude: null, + showHistorical: true, }; const module = ModuleRegistry.initModule("SimulationTimeSeriesSensitivity", defaultState); diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/queryHooks.tsx b/frontend/src/modules/SimulationTimeSeriesSensitivity/queryHooks.tsx index c15a151f3..77b890abe 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> { @@ -15,7 +20,7 @@ export function useVectorsQuery( queryFn: () => apiService.timeseries.getVectorList(caseUuid ?? "", ensembleName ?? ""), staleTime: STALE_TIME, cacheTime: CACHE_TIME, - enabled: caseUuid && ensembleName ? true : false, + enabled: !!(caseUuid && ensembleName), }); } @@ -46,7 +51,7 @@ export function useVectorDataQuery( ), staleTime: STALE_TIME, cacheTime: CACHE_TIME, - enabled: caseUuid && ensembleName && vectorName && allOrNonEmptyRealArr ? true : false, + enabled: !!(caseUuid && ensembleName && vectorName && allOrNonEmptyRealArr), }); } @@ -69,6 +74,28 @@ export function useStatisticalVectorSensitivityDataQuery( ), staleTime: STALE_TIME, cacheTime: CACHE_TIME, - enabled: allowEnable && caseUuid && ensembleName && vectorName && resampleFrequency ? true : false, + enabled: !!(allowEnable && caseUuid && ensembleName && vectorName && resampleFrequency), + }); +} + +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), }); } diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/registerModule.ts b/frontend/src/modules/SimulationTimeSeriesSensitivity/registerModule.ts index 49a1f23a9..2e81afcda 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/registerModule.ts +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/registerModule.ts @@ -6,7 +6,7 @@ import { State } from "./state"; ModuleRegistry.registerModule({ moduleName: "SimulationTimeSeriesSensitivity", - defaultTitle: "Simulation time series sensitivity", + defaultTitle: "Simulation time series per sensitivity", syncableSettingKeys: [SyncSettingKey.ENSEMBLE, SyncSettingKey.TIME_SERIES], broadcastChannelsDef, }); diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/settings.tsx b/frontend/src/modules/SimulationTimeSeriesSensitivity/settings.tsx index 729073884..04b7867b8 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/settings.tsx +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/settings.tsx @@ -1,20 +1,25 @@ import React from "react"; -import { Frequency_api, VectorDescription_api } from "@api"; +import { Frequency_api } from "@api"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { ModuleFCProps } from "@framework/Module"; import { SyncSettingKey, SyncSettingsHelper } from "@framework/SyncSettings"; import { useEnsembleSet } from "@framework/WorkbenchSession"; import { SingleEnsembleSelect } from "@framework/components/SingleEnsembleSelect"; +import { VectorSelector, createVectorSelectorDataFromVectors } from "@framework/components/VectorSelector"; import { fixupEnsembleIdent, maybeAssignFirstSyncedEnsemble } from "@framework/utils/ensembleUiHelpers"; 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 { useVectorsQuery } from "./queryHooks"; +import { isEqual } from "lodash"; + +import { useVectorListQuery } from "./queryHooks"; import { State } from "./state"; //----------------------------------------------------------------------------------------------------------- @@ -24,13 +29,18 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: console.debug(`${myInstanceIdStr} -- render SimulationTimeSeries settings`); 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 [selectedVectorTag, setSelectedVectorTag] = React.useState(null); + const [vectorSelectorData, setVectorSelectorData] = React.useState([]); + const [selectInitialVector, setSelectInitialVector] = React.useState(true); + + 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,48 +49,76 @@ 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 hasQueryData = vectorsListQuery.data !== undefined; + const vectorNames = vectorsListQuery.data?.map((vec) => vec.name) ?? []; + + // Get vector selector data + let candidateVectorSelectorData = vectorSelectorData; let candidateVectorName = selectedVectorName; + let candidateVectorTag = selectedVectorTag; + if (hasQueryData) { + candidateVectorSelectorData = createVectorSelectorDataFromVectors(vectorNames); + if (!isEqual(vectorSelectorData, candidateVectorSelectorData)) { + setVectorSelectorData(candidateVectorSelectorData); + + if (selectInitialVector) { + setSelectInitialVector(false); + const fixedUpVectorName = fixupVectorName(selectedVectorName, vectorNames); + if (fixedUpVectorName !== selectedVectorName) { + setSelectedVectorName(fixedUpVectorName); + setSelectedVectorTag(fixedUpVectorName); + candidateVectorName = fixedUpVectorName; + candidateVectorTag = fixedUpVectorName; + } + } + } + } + // Override candidates if synced if (syncedValueSummaryVector?.vectorName) { console.debug(`${myInstanceIdStr} -- syncing timeSeries to ${syncedValueSummaryVector.vectorName}`); candidateVectorName = syncedValueSummaryVector.vectorName; + candidateVectorTag = syncedValueSummaryVector.vectorName; } - const computedVectorName = fixupVectorName(candidateVectorName, vectorsQuery.data); + const computedVectorSelectorData = candidateVectorSelectorData; + const computedVectorName = candidateVectorName; + const computedVectorTag = candidateVectorTag; - if (computedEnsembleIdent && !computedEnsembleIdent.equals(selectedEnsembleIdent)) { - setSelectedEnsembleIdent(computedEnsembleIdent); - } - if (computedVectorName && computedVectorName !== selectedVectorName) { - setSelectedVectorName(computedVectorName); - } const computedEnsemble = computedEnsembleIdent ? ensembleSet.findEnsemble(computedEnsembleIdent) : null; - const sensitivities = computedEnsemble?.getSensitivities(); - - React.useEffect(() => { - const sensitivityNames = computedEnsemble?.getSensitivities()?.getSensitivityNames(); - if (sensitivityNames && sensitivityNames.length > 0) { - if (!selectedSensitivity || !sensitivityNames.includes(selectedSensitivity)) { - setSelectedSensitivity(sensitivityNames[0]); + const sensitivityNames = computedEnsemble?.getSensitivities()?.getSensitivityNames() ?? []; + React.useEffect( + function setSensitivitiesOnEnsembleChange() { + if (!isEqual(selectedSensitivities, sensitivityNames)) { + setSelectedSensitivities(sensitivityNames); } - } - }, [computedEnsemble]); - + }, + [sensitivityNames] + ); + const sensitivityOptions: SelectOption[] = sensitivityNames.map((name) => ({ + value: name, + label: name, + })); + + const hasComputedVectorName = vectorsListQuery.data?.some((vec) => vec.name === computedVectorName) ?? false; + const hasHistoricalVector = + vectorsListQuery.data?.some((vec) => vec.name === computedVectorName && vec.has_historical) ?? false; React.useEffect( function propagateVectorSpecToView() { - if (computedEnsembleIdent && computedVectorName) { + if (hasComputedVectorName && computedEnsembleIdent && computedVectorName) { moduleContext.getStateStore().setValue("vectorSpec", { ensembleIdent: computedEnsembleIdent, vectorName: computedVectorName, + hasHistorical: hasHistoricalVector, }); } else { moduleContext.getStateStore().setValue("vectorSpec", null); } }, - [computedEnsembleIdent, computedVectorName] + [computedEnsembleIdent, computedVectorName, hasComputedVectorName, hasHistoricalVector] ); function handleEnsembleSelectionChange(newEnsembleIdent: EnsembleIdent | null) { @@ -91,15 +129,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 +138,75 @@ 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.selectedNodes[0] ?? null); + setSelectedVectorTag(selection.selectedTags[0] ?? null); + } + 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,26 +214,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; -} - -function makeVectorOptionItems(vectorDescriptionsArr: VectorDescription_api[] | undefined): SelectOption[] { - const itemArr: SelectOption[] = []; - if (vectorDescriptionsArr) { - for (const vec of vectorDescriptionsArr) { - itemArr.push({ value: vec.name, label: vec.descriptive_name }); - } - } - return itemArr; + return availableVectorNames[0]; } function makeFrequencyOptionItems(): DropdownOption[] { diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/chart.tsx b/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/chart.tsx index 072fb6d6d..ff41dfd9f 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/chart.tsx +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/chart.tsx @@ -1,6 +1,8 @@ import React from "react"; import Plot from "react-plotly.js"; +import { timestampUtcMsToCompactIsoString } from "@framework/utils/timestampUtils"; + import { Layout, PlotDatum, PlotHoverEvent, PlotMouseEvent } from "plotly.js"; import { TimeSeriesPlotlyTrace } from "./traces"; @@ -13,8 +15,10 @@ export type HoverInfo = { export type TimeSeriesChartProps = { traceDataArr: TimeSeriesPlotlyTrace[]; + title: string; activeTimestampUtcMs?: number; hoveredTimestampUtcMs?: number; + uirevision?: string; onHover?: (hoverData: HoverInfo | null) => void; onClick?: (timestampUtcMs: number) => void; height?: number | 100; @@ -71,9 +75,12 @@ export const TimeSeriesChart: React.FC = (props) => { width: props.width, height: props.height, xaxis: { type: "date" }, - legend: { orientation: "h", yanchor: "bottom", y: 1.02, xanchor: "right", x: 1 }, - margin: { t: 0, b: 100, r: 0 }, + title: props.title, + legend: { orientation: "h", valign: "bottom" }, + margin: { t: 50, b: 100, r: 0 }, shapes: [], + annotations: [], + uirevision: props.uirevision, }; if (props.activeTimestampUtcMs !== undefined) { @@ -84,10 +91,19 @@ export const TimeSeriesChart: React.FC = (props) => { x0: props.activeTimestampUtcMs, y0: 0, x1: props.activeTimestampUtcMs, - y1: 1, - line: { color: "#ccc", width: 2 }, + y1: 0.99, + line: { color: "black", width: 2, dash: "dot" }, + }); + layout.annotations?.push({ + bgcolor: "white", + showarrow: false, + text: timestampUtcMsToCompactIsoString(props.activeTimestampUtcMs), + x: props.activeTimestampUtcMs, + y: 1, + yref: "paper", }); } + if (props.hoveredTimestampUtcMs !== undefined) { layout.shapes?.push({ type: "line", @@ -96,7 +112,7 @@ export const TimeSeriesChart: React.FC = (props) => { x0: props.hoveredTimestampUtcMs, y0: 0, x1: props.hoveredTimestampUtcMs, - y1: 1, + y1: 0.99, line: { color: "red", width: 1, dash: "dot" }, }); } diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/traces.ts b/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/traces.ts index 79485833d..71860b5a6 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/traces.ts +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/traces.ts @@ -1,4 +1,5 @@ -import { VectorRealizationData_api } from "@api"; +import { VectorRealizationData_api, VectorStatisticSensitivityData_api } from "@api"; +import { StatisticFunction_api } from "@api"; import { PlotData } from "plotly.js"; @@ -7,18 +8,60 @@ export interface TimeSeriesPlotlyTrace extends Partial { legendrank?: number; } +export function createStatisticalLineTraces( + sensitivityData: VectorStatisticSensitivityData_api[], + statisticsFunction: StatisticFunction_api, + color: string +): TimeSeriesPlotlyTrace[] { + const traces: TimeSeriesPlotlyTrace[] = []; + sensitivityData.forEach((aCase, index) => { + const statisticObj = aCase.value_objects.find((obj) => obj.statistic_function === statisticsFunction); + if (statisticObj) { + traces.push( + createLineTrace({ + timestampsMsUtc: aCase.timestamps_utc_ms, + values: statisticObj.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[], + sensitivityName: string, + 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:${sensitivityName}
Value: %{y}
Date: %{x}`, + }); + if (isHighlighted) { highlightedTrace = trace; } else { @@ -30,41 +73,28 @@ export function createRealizationLineTraces( } return traces; } - -function createSingleRealizationLineTrace( - vec: VectorRealizationData_api, - curveColor: string, - lineWidth: number, - lineShape: "linear" | "spline" -): TimeSeriesPlotlyTrace { - return { - x: vec.timestamps_utc_ms, - y: vec.values, - name: `real-${vec.realization}`, - realizationNumber: vec.realization, - // legendrank: vec.realization, - showlegend: false, - type: "scatter", - mode: "lines", - line: { color: curveColor, width: lineWidth, shape: lineShape }, - }; -} - -export function createSensitivityStatisticsTrace( - timestampsMsUtc: number[], - values: number[], - name: string, - lineShape: "linear" | "spline", - lineDash: "dash" | "dot" | "dashdot" | "solid", - lineColor?: string | null -): TimeSeriesPlotlyTrace { +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: timestampsMsUtc, - y: values, - name: name, - legendrank: -1, + x: data.timestampsMsUtc, + y: data.values, + name: data.name, + showlegend: data.showLegend, + hovertemplate: data.hoverTemplate, + legendgroup: data.legendGroup, type: "scatter", mode: "lines", - line: { color: lineColor || "lightblue", width: 4, dash: lineDash, shape: lineShape }, + line: { color: data.lineColor, width: data.lineWidth, dash: data.lineDash, shape: data.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..7faccbb47 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/view.tsx +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/view.tsx @@ -1,23 +1,32 @@ import React from "react"; import { StatisticFunction_api, VectorRealizationData_api, VectorStatisticSensitivityData_api } from "@api"; -import { BroadcastChannelMeta, BroadcastChannelData } from "@framework/Broadcaster"; -import { Ensemble } from "@framework/Ensemble"; +import { BroadcastChannelData, BroadcastChannelMeta } from "@framework/Broadcaster"; import { ModuleFCProps } from "@framework/Module"; import { useSubscribedValue } from "@framework/WorkbenchServices"; import { timestampUtcMsToCompactIsoString } from "@framework/utils/timestampUtils"; import { useElementSize } from "@lib/hooks/useElementSize"; +import { createSensitivityColorMap } from "@modules/_shared/sensitivityColors"; 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 { createLineTrace, createRealizationLineTraces } 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,8 +39,8 @@ 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 showHistorical = moduleContext.useStoreValue("showHistorical"); const [activeTimestampUtcMs, setActiveTimestampUtcMs] = React.useState(null); const subscribedHoverTimestampUtcMs = useSubscribedValue("global.hoverTimestamp", workbenchServices); @@ -50,9 +59,23 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod resampleFrequency, showStatistics ); + + const historicalQuery = useHistoricalVectorDataQuery( + vectorSpec?.ensembleIdent.getCaseUuid(), + vectorSpec?.ensembleIdent.getEnsembleName(), + vectorSpec?.vectorName, + resampleFrequency, + vectorSpec?.hasHistorical ? showHistorical : false + ); const ensembleSet = workbenchSession.getEnsembleSet(); const ensemble = vectorSpec ? ensembleSet.findEnsemble(vectorSpec.ensembleIdent) : null; + // Set the active timestamp to the last timestamp in the data if it is not already set + const lastTimestampUtcMs = statisticsQuery.data?.at(0)?.timestamps_utc_ms.slice(-1)[0] ?? null; + if (lastTimestampUtcMs !== null && activeTimestampUtcMs === null) { + setActiveTimestampUtcMs(lastTimestampUtcMs); + } + // Broadcast the data to the realization data channel React.useEffect( function broadcast() { @@ -82,20 +105,54 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod }, [ensemble, vectorSpec, realizationsQuery.data, activeTimestampUtcMs, moduleContext] ); + const colorSet = workbenchSettings.useColorSet(); + + const allSensitivityNamesInEnsemble = ensemble?.getSensitivities()?.getSensitivityNames().sort() ?? []; - const traceDataArr = React.useMemo(() => { - if (!ensemble || !selectedSensitivity) { - return []; + const traceDataArr: TimeSeriesPlotlyTrace[] = []; + if (ensemble && selectedSensitivities && selectedSensitivities.length > 0) { + const sensitivitiesColorMap = createSensitivityColorMap(allSensitivityNamesInEnsemble, colorSet); + selectedSensitivities.forEach((sensitivityName) => { + const color = sensitivitiesColorMap[sensitivityName]; + + // Add statistics traces + if (showStatistics && statisticsQuery.data) { + const matchingCases: VectorStatisticSensitivityData_api[] = statisticsQuery.data.filter( + (stat) => stat.sensitivity_name === sensitivityName + ); + const traces = createStatisticalLineTraces(matchingCases, StatisticFunction_api.MEAN, 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.name, color); + traceDataArr.push(...traces); + } + } + }); + // Add history + if (historicalQuery?.data && showHistorical) { + 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 +183,8 @@ 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; -} diff --git a/frontend/src/modules/Sensitivity/loadModule.tsx b/frontend/src/modules/TornadoChart/loadModule.tsx similarity index 81% rename from frontend/src/modules/Sensitivity/loadModule.tsx rename to frontend/src/modules/TornadoChart/loadModule.tsx index bbd363c28..3b73df84a 100644 --- a/frontend/src/modules/Sensitivity/loadModule.tsx +++ b/frontend/src/modules/TornadoChart/loadModule.tsx @@ -10,7 +10,7 @@ const defaultState: State = { responseChannelName: null, }; -const module = ModuleRegistry.initModule("Sensitivity", defaultState); +const module = ModuleRegistry.initModule("TornadoChart", defaultState); module.viewFC = view; module.settingsFC = settings; diff --git a/frontend/src/modules/Sensitivity/preview.tsx b/frontend/src/modules/TornadoChart/preview.tsx similarity index 100% rename from frontend/src/modules/Sensitivity/preview.tsx rename to frontend/src/modules/TornadoChart/preview.tsx diff --git a/frontend/src/modules/Sensitivity/registerModule.tsx b/frontend/src/modules/TornadoChart/registerModule.tsx similarity index 79% rename from frontend/src/modules/Sensitivity/registerModule.tsx rename to frontend/src/modules/TornadoChart/registerModule.tsx index bc5fb4795..477d7a1c6 100644 --- a/frontend/src/modules/Sensitivity/registerModule.tsx +++ b/frontend/src/modules/TornadoChart/registerModule.tsx @@ -5,8 +5,8 @@ import { preview } from "./preview"; import { State } from "./state"; ModuleRegistry.registerModule({ - moduleName: "Sensitivity", - defaultTitle: "Sensitivity", + moduleName: "TornadoChart", + defaultTitle: "Tornado Chart", syncableSettingKeys: [SyncSettingKey.ENSEMBLE, SyncSettingKey.TIME_SERIES], - preview + preview, }); diff --git a/frontend/src/modules/Sensitivity/sensitivityChart.tsx b/frontend/src/modules/TornadoChart/sensitivityChart.tsx similarity index 94% rename from frontend/src/modules/Sensitivity/sensitivityChart.tsx rename to frontend/src/modules/TornadoChart/sensitivityChart.tsx index 47d733a7a..6fe8b4c9b 100644 --- a/frontend/src/modules/Sensitivity/sensitivityChart.tsx +++ b/frontend/src/modules/TornadoChart/sensitivityChart.tsx @@ -1,13 +1,20 @@ import React, { useEffect, useState } from "react"; import Plot from "react-plotly.js"; +import { SensitivityColorMap } from "@modules/_shared/sensitivityColors"; + 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; + sensitivityColorMap: SensitivityColorMap; showLabels: boolean; hideZeroY: boolean; showRealizationPoints: boolean; @@ -102,16 +109,8 @@ const lowTrace = ( sensitivityResponses: SensitivityResponse[], showLabels: boolean, selectedBar: SelectedBar | null, - barColor = "e53935" + sensitivitiesColorMap: SensitivityColorMap ): 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 { @@ -126,9 +125,10 @@ const lowTrace = ( textposition: "auto", insidetextanchor: "middle", name: TraceGroup.LOW, + showlegend: false, orientation: "h", marker: { - color: barColor, + color: sensitivityResponses.map((s) => sensitivitiesColorMap[s.sensitivityName]), line: { width: 3, color: sensitivityResponses.map((s, idx) => @@ -146,7 +146,7 @@ const highTrace = ( sensitivityResponses: SensitivityResponse[], showLabels: boolean, selectedBar: SelectedBar | null, - barColor = "#00897b" + sensitivitiesColorMap: SensitivityColorMap ): Partial => { // const label = showLabels // ? sensitivityResponses.map( @@ -170,9 +170,10 @@ const highTrace = ( insidetextanchor: "middle", type: "bar", name: TraceGroup.HIGH, + showlegend: false, orientation: "h", marker: { - color: barColor, + color: sensitivityResponses.map((s) => sensitivitiesColorMap[s.sensitivityName]), line: { width: 3, color: sensitivityResponses.map((s, idx) => @@ -202,8 +203,8 @@ const sensitivityChart: React.FC = (props) => { ); } - traces.push(lowTrace(filteredSensitivityResponses, showLabels, selectedBar)); - traces.push(highTrace(filteredSensitivityResponses, showLabels, selectedBar)); + traces.push(lowTrace(filteredSensitivityResponses, showLabels, selectedBar, props.sensitivityColorMap)); + traces.push(highTrace(filteredSensitivityResponses, showLabels, selectedBar, props.sensitivityColorMap)); // if (showRealizationPoints) { // TODO: Add realization points diff --git a/frontend/src/modules/Sensitivity/sensitivityResponseCalculator.ts b/frontend/src/modules/TornadoChart/sensitivityResponseCalculator.ts similarity index 100% rename from frontend/src/modules/Sensitivity/sensitivityResponseCalculator.ts rename to frontend/src/modules/TornadoChart/sensitivityResponseCalculator.ts diff --git a/frontend/src/modules/Sensitivity/sensitivityTable.tsx b/frontend/src/modules/TornadoChart/sensitivityTable.tsx similarity index 100% rename from frontend/src/modules/Sensitivity/sensitivityTable.tsx rename to frontend/src/modules/TornadoChart/sensitivityTable.tsx diff --git a/frontend/src/modules/Sensitivity/settings.tsx b/frontend/src/modules/TornadoChart/settings.tsx similarity index 93% rename from frontend/src/modules/Sensitivity/settings.tsx rename to frontend/src/modules/TornadoChart/settings.tsx index 07015498e..507badea9 100644 --- a/frontend/src/modules/Sensitivity/settings.tsx +++ b/frontend/src/modules/TornadoChart/settings.tsx @@ -12,7 +12,7 @@ export function settings({ moduleContext, workbenchServices, initialSettings }: return ( <> -