diff --git a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/interfaces.ts b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/interfaces.ts index 35a4d4f56..15970247e 100644 --- a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/interfaces.ts +++ b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/interfaces.ts @@ -2,11 +2,11 @@ import type { ParameterIdent } from "@framework/EnsembleParameters"; import type { InterfaceInitialization } from "@framework/UniDirectionalModuleComponentsInterface"; import { - parameterIdentsAtom, showLabelsAtom, useFixedColorRangeAtom, plotTypeAtom, correlationSettingsAtom, + selectedParameterIdentsAtom, } from "./settings/atoms/baseAtoms"; import type { PlotType, CorrelationSettings } from "./typesAndEnums"; @@ -23,7 +23,7 @@ export type Interfaces = { }; export const settingsToViewInterfaceInitialization: InterfaceInitialization = { - parameterIdents: (get) => get(parameterIdentsAtom), + parameterIdents: (get) => get(selectedParameterIdentsAtom), showLabels: (get) => get(showLabelsAtom), useFixedColorRange: (get) => get(useFixedColorRangeAtom), plotType: (get) => get(plotTypeAtom), diff --git a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/settings/atoms/baseAtoms.ts b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/settings/atoms/baseAtoms.ts index dee211280..1a33a10f8 100644 --- a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/settings/atoms/baseAtoms.ts +++ b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/settings/atoms/baseAtoms.ts @@ -1,14 +1,16 @@ import { atom } from "jotai"; import type { ParameterIdent } from "@framework/EnsembleParameters"; +import type { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; +import type { KeyKind, ChannelReceiverReturnData } from "@framework/types/dataChannnel"; import { PlotType, type CorrelationSettings } from "../../typesAndEnums"; - -export const parameterIdentsAtom = atom([]); +export const receivedChannelAtom = atom[]>([]); +export const selectedParameterIdentsAtom = atom([]); export const showLabelsAtom = atom(false); export const useFixedColorRangeAtom = atom(true); -export const plotTypeAtom = atom(PlotType.ParameterResponseMatrix); - +export const plotTypeAtom = atom(PlotType.FullTriangularMatrix); +export const regularEnsembleIdentsAtom = atom([]); export const correlationSettingsAtom = atom({ threshold: null, hideIndividualCells: true, diff --git a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/settings/atoms/derivedAtoms.ts b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/settings/atoms/derivedAtoms.ts new file mode 100644 index 000000000..6a66e08c2 --- /dev/null +++ b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/settings/atoms/derivedAtoms.ts @@ -0,0 +1,34 @@ +import { atom } from "jotai"; + +import { EnsembleSetAtom } from "@framework/GlobalAtoms"; +import { RegularEnsemble } from "@framework/RegularEnsemble"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; +import { getContinuousAndNonConstantParameterIdentsInEnsembles } from "@modules/_shared/parameterUnions"; + +import { receivedChannelAtom } from "./baseAtoms"; + +export const availableParameterIdentsAtom = atom((get) => { + const receivedChannels = get(receivedChannelAtom); + if (!receivedChannels) { + return []; + } + + // Filter for channels that have content + const channelsWithContent = receivedChannels.filter((response) => response.channel?.contents); + if (!channelsWithContent.length) { + return []; + } + + // Extract ensemble identifiers from channels with content + const ensembleIdentStrings = channelsWithContent + .flatMap((channel) => channel.channel?.contents || []) + .map((content) => content.metaData.ensembleIdentString); + + // Get regular ensemble identifiers + const ensembleSet = get(EnsembleSetAtom); + const regularEnsembleIdents = ensembleIdentStrings + .map((id) => ensembleSet.findEnsembleByIdentString(id)) + .filter((ensemble) => ensemble instanceof RegularEnsemble) + .map((ensemble) => RegularEnsembleIdent.fromString(ensemble.getIdent().toString())); + return getContinuousAndNonConstantParameterIdentsInEnsembles(ensembleSet, regularEnsembleIdents); +}); diff --git a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/settings/settings.tsx b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/settings/settings.tsx index 93a0e1fb7..fa8d67577 100644 --- a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/settings/settings.tsx +++ b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/settings/settings.tsx @@ -1,30 +1,29 @@ import React from "react"; -import { useAtom } from "jotai"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import type { ParameterIdent } from "@framework/EnsembleParameters"; import { useApplyInitialSettingsToState } from "@framework/InitialSettings"; import type { ModuleSettingsProps } from "@framework/Module"; -import { RegularEnsemble } from "@framework/RegularEnsemble"; -import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { KeyKind } from "@framework/types/dataChannnel"; import { Checkbox } from "@lib/components/Checkbox"; import { CollapsibleGroup } from "@lib/components/CollapsibleGroup"; import { Label } from "@lib/components/Label"; import { RadioGroup } from "@lib/components/RadioGroup"; import { ParametersSelector } from "@modules/_shared/components/ParameterSelector"; -import { getContinuousAndNonConstantParameterIdentsInEnsembles } from "@modules/_shared/parameterUnions"; import type { Interfaces } from "../interfaces"; import { PlotType } from "../typesAndEnums"; import { correlationSettingsAtom, - parameterIdentsAtom, plotTypeAtom, showLabelsAtom, useFixedColorRangeAtom, + receivedChannelAtom, + selectedParameterIdentsAtom, } from "./atoms/baseAtoms"; +import { availableParameterIdentsAtom } from "./atoms/derivedAtoms"; const plotTypesOptions = [ { @@ -41,44 +40,51 @@ const plotTypesOptions = [ }, ]; -export function Settings({ initialSettings, settingsContext, workbenchSession }: ModuleSettingsProps) { - const [parameterIdents, setParameterIdents] = useAtom(parameterIdentsAtom); +export function Settings({ initialSettings, settingsContext }: ModuleSettingsProps) { + const [selectedParameterIdents, setSelectedParameterIdents] = useAtom(selectedParameterIdentsAtom); const [plotType, setPlotType] = useAtom(plotTypeAtom); const [showLabels, setShowLabels] = useAtom(showLabelsAtom); const [useFixedColorRange, setUseFixedColorRange] = useAtom(useFixedColorRangeAtom); const [correlationSettings, setCorrelationSettings] = useAtom(correlationSettingsAtom); + const setReceivedChannel = useSetAtom(receivedChannelAtom); + const availableParameterIdents = useAtomValue(availableParameterIdentsAtom); - useApplyInitialSettingsToState(initialSettings, "parameterIdents", "array", setParameterIdents); + useApplyInitialSettingsToState(initialSettings, "selectedParameterIdents", "array", setSelectedParameterIdents); useApplyInitialSettingsToState(initialSettings, "showLabels", "boolean", setShowLabels); useApplyInitialSettingsToState(initialSettings, "correlationSettings", "object", setCorrelationSettings); - const receiverResponse = settingsContext.useChannelReceiver({ + + const receiverResponse1 = settingsContext.useChannelReceiver({ receiverIdString: "channelResponse", expectedKindsOfKeys: [KeyKind.REALIZATION], }); + const receiverResponse2 = settingsContext.useChannelReceiver({ + receiverIdString: "channelResponse2", + expectedKindsOfKeys: [KeyKind.REALIZATION], + }); + const receiverResponse3 = settingsContext.useChannelReceiver({ + receiverIdString: "channelResponse3", + expectedKindsOfKeys: [KeyKind.REALIZATION], + }); + const receiverResponses = React.useMemo( + () => [receiverResponse1, receiverResponse2, receiverResponse3], + [receiverResponse1, receiverResponse2, receiverResponse3], + ); - const ensembleIdentStringsFromChannels: string[] = React.useMemo(() => { - if (receiverResponse.channel && receiverResponse.channel.contents) { - return receiverResponse.channel.contents.map((content) => content.metaData.ensembleIdentString); - } - return []; - }, [receiverResponse.channel]); - - const ensembleSet = workbenchSession.getEnsembleSet(); - - const regularEnsembleIdentsFromChannels: RegularEnsembleIdent[] = React.useMemo(() => { - return ensembleIdentStringsFromChannels.flatMap((id) => { - const ensemble = ensembleSet.findEnsembleByIdentString(id); - return ensemble instanceof RegularEnsemble ? [RegularEnsembleIdent.fromString(id)] : []; - }); - }, [ensembleIdentStringsFromChannels, ensembleSet]); - - const allParameterIdents = getContinuousAndNonConstantParameterIdentsInEnsembles( - ensembleSet, - regularEnsembleIdentsFromChannels, + React.useEffect( + () => { + setReceivedChannel(receiverResponses); + }, // We only want to listen to revision number changes, but we need the whole channel response to set it + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + receiverResponse1.revisionNumber, + receiverResponse2.revisionNumber, + receiverResponse3.revisionNumber, + setReceivedChannel, + ], ); function handleParametersChanged(parameterIdents: ParameterIdent[]) { - setParameterIdents(parameterIdents); + setSelectedParameterIdents(parameterIdents); } function handleShowLabelsChanged(e: React.ChangeEvent) { setShowLabels(e.target.checked); @@ -168,8 +174,8 @@ export function Settings({ initialSettings, settingsContext, workbenchSession }: diff --git a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/typesAndEnums.ts b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/typesAndEnums.ts index b041ba7df..0f70393c2 100644 --- a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/typesAndEnums.ts +++ b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/typesAndEnums.ts @@ -10,3 +10,4 @@ export type CorrelationSettings = { filterColumns: boolean; filterRows: boolean; }; +export const MAX_NUMBER_OF_PARAMETERS_IN_MATRIX = 500; // To avoid performance issues diff --git a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/view/utils/parameterCorrelationMatrixFigure.ts b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/view/utils/parameterCorrelationMatrixFigure.ts index 0070048a1..9aec3c6ab 100644 --- a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/view/utils/parameterCorrelationMatrixFigure.ts +++ b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/view/utils/parameterCorrelationMatrixFigure.ts @@ -57,7 +57,7 @@ export class ParameterCorrelationMatrixFigure { showlegend: false, font: { family: "Roboto, sans-serif", - size: 12, + size: 10, color: "#333", }, }); @@ -78,11 +78,11 @@ export class ParameterCorrelationMatrixFigure { }; // Always show response labels if (this._forceShowYAxisLabels) { - margin.l = 200; + margin.l = 100; } if (this._showLabels) { - margin.l = 200; - margin.b = 200; + margin.l = 100; + margin.b = 100; } this._figure.updateLayout({ margin: margin, diff --git a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/view/view.tsx b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/view/view.tsx index 15bd97437..653d110df 100644 --- a/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/view/view.tsx +++ b/frontend/src/modules/ParameterResponseCorrelationMatrixPlot/view/view.tsx @@ -27,7 +27,7 @@ import { } from "@modules/_shared/utils/math/correlationMatrix"; import type { Interfaces } from "../interfaces"; -import { PlotType, type CorrelationSettings } from "../typesAndEnums"; +import { MAX_NUMBER_OF_PARAMETERS_IN_MATRIX, PlotType, type CorrelationSettings } from "../typesAndEnums"; import { ParameterCorrelationMatrixFigure } from "./utils/parameterCorrelationMatrixFigure"; import { createResponseParameterCorrelationMatrix } from "./utils/parameterCorrelationMatrixUtils"; @@ -93,6 +93,7 @@ export function View({ viewContext, workbenchSession, workbenchSettings }: Modul statusWriter.setLoading(isPending || receiverResponses.some((r) => r.isPending)); const receiverResponseRevisionNumbers = receiverResponses.map((response) => response.revisionNumber); + const hasParameterIdentsChanged = parameterIdents.length !== prevParameterIdents.length || !parameterIdents.every((ident, index) => ident.equals(prevParameterIdents[index])); @@ -118,24 +119,49 @@ export function View({ viewContext, workbenchSession, workbenchSettings }: Modul startTransition(function makeContent() { // Content when no data channels are defined + if (receiverResponses.every((response) => !response.channel)) { setContent( - - Data channel required for use. Add a main module to the workbench and use the data channels - - {" "} - Up to 3 modules can be connected. - - - - - +
+

Data channel required for use.

+

Add a module supporting data channels to the dashboard and connect it to this module.

+

+ Modules supporting data channels have an icon on their + toolbar. +

+

Drag from this icon to a response below:

+
+ + + +
+
+
, + ); + return; + } + if (parameterIdents.length === 0) { + setContent( + + + No parameters selected or available. Please select parameters in the settings pane. If no + parameters are available, ensure that the connected ensembles have continuous and varying + parameters. + , + ); + return; + } + if (parameterIdents.length > MAX_NUMBER_OF_PARAMETERS_IN_MATRIX) { + setContent( + + + {`Too many parameters selected. Please select ${MAX_NUMBER_OF_PARAMETERS_IN_MATRIX} or fewer parameters to display the correlation + matrix.`} , ); return; } - const usedChannels = receiverResponses.filter((response) => response.channel); const usedChannelsWithoutData = receiverResponses.filter( (response) => response.channel && response.channel.contents.length === 0, @@ -172,6 +198,31 @@ export function View({ viewContext, workbenchSession, workbenchSettings }: Modul receiveResponsesPerEnsembleIdent.get(ensembleIdentString)?.push(content); }); }); + for (const ensembleIdentString of receiveResponsesPerEnsembleIdent.keys()) { + const ensemble = ensembleSet.findEnsembleByIdentString(ensembleIdentString); + if (!ensemble || ensemble instanceof DeltaEnsemble) { + const ensembleType = !ensemble ? "Invalid" : "Delta"; + setContent( + +

{ensembleType} ensemble detected in the data channel.

+

Unable to compute parameter correlations.

+
, + ); + return; + } + } + // Content when no parameters are selected + if (parameterIdents.length === 0) { + setContent( + + + No parameters selected or available. Please select parameters in the settings pane. If + parameters are selected but not available, ensure that the connected ensembles have continuous + and varying parameters. + , + ); + return; + } const numContents = receiveResponsesPerEnsembleIdent.size; @@ -189,19 +240,7 @@ export function View({ viewContext, workbenchSession, workbenchSettings }: Modul showLabels, useFixedColorRange, }); - for (const ensembleIdentString of receiveResponsesPerEnsembleIdent.keys()) { - const ensemble = ensembleSet.findEnsembleByIdentString(ensembleIdentString); - if (!ensemble || ensemble instanceof DeltaEnsemble) { - const ensembleType = !ensemble ? "Invalid" : "Delta"; - setContent( - -

{ensembleType} ensemble detected in the data channel.

-

Unable to compute parameter correlations.

-
, - ); - return; - } - } + fillParameterCorrelationMatrixFigure( figure, parameterIdents, diff --git a/frontend/src/modules/_shared/components/ParameterSelector.tsx b/frontend/src/modules/_shared/components/ParameterSelector.tsx index 0962e1eb6..32b67ae1a 100644 --- a/frontend/src/modules/_shared/components/ParameterSelector.tsx +++ b/frontend/src/modules/_shared/components/ParameterSelector.tsx @@ -22,19 +22,29 @@ export function ParametersSelector({ onChange, }: ParametersSelectorProps): React.ReactNode { const [autoSelectAllOnGroupChange, setAutoSelectAllOnGroupChange] = React.useState(true); + const [userHasInteracted, setUserHasInteracted] = React.useState(false); + const [selectedGroupFilterValues, setSelectedGroupFilterValues] = React.useState([]); - const [selectedGroupFilterValues, setSelectedGroupFilterValues] = React.useState(() => { - if (selectedParameterIdents.length > 0) { - return Array.from(new Set(selectedParameterIdents.map((p) => p.groupName ?? GroupType.NO_GROUP))); + React.useEffect(() => { + if (selectedGroupFilterValues.length === 0 && selectedParameterIdents.length > 0) { + setSelectedGroupFilterValues( + Array.from(new Set(selectedParameterIdents.map((p) => p.groupName ?? GroupType.NO_GROUP))), + ); } - return []; - }); + }, [selectedParameterIdents, selectedGroupFilterValues]); + React.useEffect(() => { + if (!userHasInteracted && allParameterIdents.length > 0) { + const allGroups = Array.from(new Set(allParameterIdents.map((p) => p.groupName ?? GroupType.NO_GROUP))); + setSelectedGroupFilterValues(allGroups); + onChange(allParameterIdents); + } + }, [allParameterIdents, userHasInteracted, onChange]); const handleGroupChange = (newlySelectedGroupFilterStrings: string[]) => { setSelectedGroupFilterValues(newlySelectedGroupFilterStrings); if (newlySelectedGroupFilterStrings.length === 0) { - onChange([]); + handleChange([]); } else { const parametersThatMatchNewGroups = allParameterIdents.filter((p) => newlySelectedGroupFilterStrings.some( @@ -50,14 +60,20 @@ export function ParametersSelector({ newSelectedParameters = selectedParameterIdents.filter((p) => parametersThatMatchNewGroups.some((pg) => pg.equals(p)), ); + if (newSelectedParameters.length === 0 && parametersThatMatchNewGroups.length > 0) { + newSelectedParameters = [parametersThatMatchNewGroups[0]]; + } } - onChange(newSelectedParameters); + handleChange(newSelectedParameters); } }; - + const handleChange = (newlySelectedParameters: ParameterIdent[]) => { + !userHasInteracted && setUserHasInteracted(true); + onChange(newlySelectedParameters); + }; const handleParameterChange = (selectedValues: string[]) => { - onChange(selectedValues.map((s) => ParameterIdent.fromString(s))); + handleChange(selectedValues.map((s) => ParameterIdent.fromString(s))); }; const groupSelectOptions: SelectOption[] = Array.from( diff --git a/frontend/src/templates/correlationMatrixTimeSeries.ts b/frontend/src/templates/correlationMatrixTimeSeries.ts index ce9dd16d6..ac290acea 100644 --- a/frontend/src/templates/correlationMatrixTimeSeries.ts +++ b/frontend/src/templates/correlationMatrixTimeSeries.ts @@ -1,4 +1,3 @@ -import { ParameterIdent } from "@framework/EnsembleParameters"; import { SyncSettingKey } from "@framework/SyncSettings"; import type { Template } from "@framework/TemplateRegistry"; import { TemplateRegistry } from "@framework/TemplateRegistry"; @@ -65,20 +64,6 @@ const template: Template = { }, }, initialSettings: { - parameterIdents: [ - new ParameterIdent("KVKH_CHANNEL", "GLOBVAR"), - new ParameterIdent("KVKH_CREVASSE", "GLOBVAR"), - new ParameterIdent("KVKH_US", "GLOBVAR"), - new ParameterIdent("KVKH_LS", "GLOBVAR"), - new ParameterIdent("FWL_CENTRAL", "GLOBVAR"), - new ParameterIdent("FWL_NORTH_HORST", "GLOBVAR"), - new ParameterIdent("GOC_NORTH_HORST", "GLOBVAR"), - new ParameterIdent("RELPERM_INT_WO", "GLOBVAR"), - new ParameterIdent("RELPERM_INT_GO", "GLOBVAR"), - new ParameterIdent("ISOTREND_ALT1W_VALYSAR", "GLOBVAR"), - new ParameterIdent("ISOTREND_ALT1W_THERYS", "GLOBVAR"), - new ParameterIdent("ISOTREND_ALT1W_VOLON", "GLOBVAR"), - ], showLabels: true, correlationSettings: { hideIndividualCells: true,