Skip to content

Commit

Permalink
SimulationTimeSeriesMatrix module (equinor#293)
Browse files Browse the repository at this point in the history
  • Loading branch information
jorgenherje authored Sep 6, 2023
1 parent 9c75b4d commit e0419b3
Show file tree
Hide file tree
Showing 21 changed files with 2,251 additions and 3 deletions.
21 changes: 21 additions & 0 deletions frontend/src/framework/WorkbenchSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,27 @@ const defaultColorPalettes = [
],
id: "resinsight",
}),
new ColorPalette({
name: "Time Series",
colors: [
"#1F77B4",
"#FF7F0E",
"#2CA02C",
"#D62728",
"#9467BD",
"#8C564B",
"#E377C2",
"#7F7F7F",
"#BCBD22",
"#17BECF",
],
id: "time-series",
}),
new ColorPalette({
name: "Dutch Field",
colors: ["#e60049", "#0bb4ff", "#50e991", "#e6d800", "#9b19f5", "#ffa300", "#dc0ab4", "#b3d4ff", "#00bfa0"],
id: "dutch-field",
}),
];

const defaultContinuousSequentialColorPalettes = [
Expand Down
29 changes: 27 additions & 2 deletions frontend/src/framework/utils/ensembleUiHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ export function maybeAssignFirstSyncedEnsemble(
/**
* Validates the the EnsembleIdent specified in currIdent against the contents of the
* EnsembleSet and fixes the value if it isn't valid.
*
*
* Returns null if an empty EnsembleSet is specified.
*
*
* Note that if the specified EnsembleIdent is valid, this function will always return
* a reference to the exact same object that was passed in currIdent. This means that
* you can compare the references (fixedIdent !== currIdent) to detect any changes.
Expand All @@ -43,3 +43,28 @@ export function fixupEnsembleIdent(

return ensembleSet.getEnsembleArr()[0].getIdent();
}

/**
* Validates the the EnsembleIdents specified in currIdents against the contents of the
* EnsembleSet and fixes the value if it isn't valid.
*
* Returns null if an empty EnsembleSet is specified.
*
* Note that if the specified EnsembleIdents are valid, this function will always return
* a reference to the exact same object that was passed in currIdent. This means that
* you can compare the references (fixedIdent !== currIdent) to detect any changes.
*/
export function fixupEnsembleIdents(
currIdents: EnsembleIdent[] | null,
ensembleSet: EnsembleSet | null
): EnsembleIdent[] | null {
if (!ensembleSet?.hasAnyEnsembles()) {
return null;
}

if (currIdents === null || currIdents.length === 0) {
return [ensembleSet.getEnsembleArr()[0].getIdent()];
}

return currIdents.filter((currIdent) => ensembleSet.findEnsemble(currIdent));
}
41 changes: 41 additions & 0 deletions frontend/src/lib/components/ApiStatesWrapper/apiStatesWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from "react";

import { resolveClassNames } from "@lib/utils/resolveClassNames";
import { QueryObserverResult } from "@tanstack/react-query";

export type ApiStatesWrapperProps = {
apiResults: QueryObserverResult[];
loadingComponent: React.ReactNode;
errorComponent: React.ReactNode;
className?: string;
style?: React.CSSProperties;
children: React.ReactNode;
};

export const ApiStatesWrapper: React.FC<ApiStatesWrapperProps> = (props: ApiStatesWrapperProps) => {
return (
<div
className={resolveClassNames(
"relative rounded",
{ "outline outline-blue-100 outline-offset-2": props.apiResults.some((elm) => elm.isLoading) },
{ "outline outline-red-100 outline-offset-2": props.apiResults.some((elm) => elm.isError) },
props.className ?? ""
)}
style={props.style}
>
{props.apiResults.some((elm) => elm.isLoading) && (
<div className="absolute left-0 right-0 w-full h-full bg-white bg-opacity-80 flex items-center justify-center z-10">
{props.loadingComponent}
</div>
)}
{props.apiResults.some((elm) => elm.isError) && (
<div className="absolute left-0 right-0 w-full h-full bg-white bg-opacity-80 flex items-center justify-center z-10">
{props.errorComponent}
</div>
)}
{props.children}
</div>
);
};

ApiStatesWrapper.displayName = "ApiStatesWrapper";
1 change: 1 addition & 0 deletions frontend/src/lib/components/ApiStatesWrapper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ApiStatesWrapper } from "./apiStatesWrapper";
46 changes: 46 additions & 0 deletions frontend/src/lib/utils/vectorSelectorUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { TreeDataNode } from "@lib/components/SmartNodeSelector";

export function addVectorToVectorSelectorData(
vectorSelectorData: TreeDataNode[],
vector: string,
description?: string,
descriptionAtLastNode = false
): void {
const nodes = vector.split(":");
let currentChildList = vectorSelectorData;

nodes.forEach((node, index) => {
let foundNode = false;
for (const child of currentChildList) {
if (child.name === node) {
foundNode = true;
currentChildList = child.children ?? [];
break;
}
}
if (!foundNode) {
const doAddDescription =
description !== undefined &&
((descriptionAtLastNode && index === nodes.length - 1) || (!descriptionAtLastNode && index === 0));

const nodeData: TreeDataNode = {
name: node,
children: index < nodes.length - 1 ? [] : undefined,
description: doAddDescription ? description : undefined,
};

currentChildList.push(nodeData);
currentChildList = nodeData.children ?? [];
}
});
}

export function createVectorSelectorDataFromVectors(vectors: string[]): TreeDataNode[] {
const vectorSelectorData: TreeDataNode[] = [];

for (const vector of vectors) {
addVectorToVectorSelectorData(vectorSelectorData, vector);
}

return vectorSelectorData;
}
25 changes: 25 additions & 0 deletions frontend/src/modules/SimulationTimeSeriesMatrix/loadModule.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Frequency_api, StatisticFunction_api } from "@api";
import { ModuleRegistry } from "@framework/ModuleRegistry";

import { settings } from "./settings";
import { FanchartStatisticOption, GroupBy, State, VisualizationMode } from "./state";
import { view } from "./view";

const defaultState: State = {
groupBy: GroupBy.TIME_SERIES,
visualizationMode: VisualizationMode.INDIVIDUAL_REALIZATIONS,
vectorSpecifications: [],
resamplingFrequency: Frequency_api.MONTHLY,
showObservations: true,
showHistorical: true,
statisticsSelection: {
IndividualStatisticsSelection: Object.values(StatisticFunction_api),
FanchartStatisticsSelection: Object.values(FanchartStatisticOption),
},
realizationsToInclude: null,
};

const module = ModuleRegistry.initModule<State>("SimulationTimeSeriesMatrix", defaultState);

module.viewFC = view;
module.settingsFC = settings;
48 changes: 48 additions & 0 deletions frontend/src/modules/SimulationTimeSeriesMatrix/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { DrawPreviewFunc } from "@framework/Preview";

export const preview: DrawPreviewFunc = function (width: number, height: number) {
const paths: {
x1: number;
y1: number;
x2: number;
y2: number;
x3: number;
y3: number;
xc: number;
yc: number;
color: string;
}[] = [];
const numPaths = 9;
for (let i = 0; i < numPaths; i++) {
const x1 = 0;
const y1 = height - (i / numPaths) * height;
const x2 = width / 2;
const y2 = height - ((i - 1) / numPaths) * height;
const x3 = width;
const y3 = height - (((i - 1) / numPaths) * height) / 1.2;
const xc = width / 4;
const yc = height - (i / numPaths) * height - height / 12;

// Assign colors based on position
const color = i < 3 ? "green" : i < 6 ? "red" : "blue";

paths.push({ x1, y1, x2, y2, x3, y3, xc, yc, color });
}
return (
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
<path d={`M 0 ${height} L 0 0`} stroke="black" strokeWidth={1} />
<path d={`M 0 ${height} L ${width} ${height}`} stroke="black" strokeWidth={1} />
{paths.map((path, index) => {
return (
<path
key={index}
d={`M ${path.x1} ${path.y1} Q ${path.xc} ${path.yc} ${path.x2} ${path.y2} T ${path.x3} ${path.y3}`}
fill="none"
stroke={path.color} // Set stroke color
strokeWidth={1}
/>
);
})}
</svg>
);
};
143 changes: 143 additions & 0 deletions frontend/src/modules/SimulationTimeSeriesMatrix/queryHooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { Frequency_api, VectorDescription_api } from "@api";
import { VectorHistoricalData_api, VectorRealizationData_api, VectorStatisticData_api } from "@api";
import { apiService } from "@framework/ApiService";
import { EnsembleIdent } from "@framework/EnsembleIdent";
import { UseQueryResult, useQueries } from "@tanstack/react-query";

import { VectorSpec } from "./state";

const STALE_TIME = 60 * 1000;
const CACHE_TIME = 60 * 1000;

export function useVectorListQueries(
caseUuidsAndEnsembleNames: EnsembleIdent[] | null
): UseQueryResult<VectorDescription_api[]>[] {
// Note: how to cancel queryFn if key is updated?
return useQueries({
queries: (caseUuidsAndEnsembleNames ?? []).map((item) => {
return {
queryKey: ["getVectorList", item.getCaseUuid(), item.getEnsembleName()],
queryFn: () =>
apiService.timeseries.getVectorList(item.getCaseUuid() ?? "", item.getEnsembleName() ?? ""),
staleTime: STALE_TIME,
cacheTime: CACHE_TIME,
enabled: item.getCaseUuid() && item.getEnsembleName() ? true : false,
};
}),
});
}

export function useVectorDataQueries(
vectorSpecifications: VectorSpec[] | null,
resampleFrequency: Frequency_api | null,
realizationsToInclude: number[] | null,
allowEnable: boolean
): UseQueryResult<VectorRealizationData_api[]>[] {
// Note: how to cancel queryFn if key is updated?
return useQueries({
queries: (vectorSpecifications ?? []).map((item) => {
return {
queryKey: [
"getRealizationsVectorData",
item.ensembleIdent.getCaseUuid(),
item.ensembleIdent.getEnsembleName(),
item.vectorName,
resampleFrequency,
realizationsToInclude,
],
queryFn: () =>
apiService.timeseries.getRealizationsVectorData(
item.ensembleIdent.getCaseUuid() ?? "",
item.ensembleIdent.getEnsembleName() ?? "",
item.vectorName ?? "",
resampleFrequency ?? undefined,
realizationsToInclude ?? undefined
),
staleTime: STALE_TIME,
cacheTime: CACHE_TIME,
enabled: !!(
allowEnable &&
item.vectorName &&
item.ensembleIdent.getCaseUuid() &&
item.ensembleIdent.getEnsembleName()
),
};
}),
});
}

export function useStatisticalVectorDataQueries(
vectorSpecifications: VectorSpec[] | null,
resampleFrequency: Frequency_api | null,
realizationsToInclude: number[] | null,
allowEnable: boolean
): UseQueryResult<VectorStatisticData_api>[] {
return useQueries({
queries: (vectorSpecifications ?? []).map((item) => {
return {
queryKey: [
"getStatisticalVectorData",
item.ensembleIdent.getCaseUuid(),
item.ensembleIdent.getEnsembleName(),
item.vectorName,
resampleFrequency,
realizationsToInclude,
],
queryFn: () =>
apiService.timeseries.getStatisticalVectorData(
item.ensembleIdent.getCaseUuid() ?? "",
item.ensembleIdent.getEnsembleName() ?? "",
item.vectorName ?? "",
resampleFrequency ?? Frequency_api.MONTHLY,
undefined,
realizationsToInclude ?? undefined
),
staleTime: STALE_TIME,
cacheTime: CACHE_TIME,
enabled: !!(
allowEnable &&
item.vectorName &&
item.ensembleIdent.getCaseUuid() &&
item.ensembleIdent.getEnsembleName() &&
resampleFrequency
),
};
}),
});
}

export function useHistoricalVectorDataQueries(
nonHistoricalVectorSpecifications: VectorSpec[] | null,
resampleFrequency: Frequency_api | null,
allowEnable: boolean
): UseQueryResult<VectorHistoricalData_api>[] {
return useQueries({
queries: (nonHistoricalVectorSpecifications ?? []).map((item) => {
return {
queryKey: [
"getHistoricalVectorData",
item.ensembleIdent.getCaseUuid(),
item.ensembleIdent.getEnsembleName(),
item.vectorName,
resampleFrequency,
],
queryFn: () =>
apiService.timeseries.getHistoricalVectorData(
item.ensembleIdent.getCaseUuid() ?? "",
item.ensembleIdent.getEnsembleName() ?? "",
item.vectorName ?? "",
resampleFrequency ?? Frequency_api.MONTHLY
),
staleTime: STALE_TIME,
cacheTime: CACHE_TIME,
enabled: !!(
allowEnable &&
item.vectorName &&
item.ensembleIdent.getCaseUuid() &&
item.ensembleIdent.getEnsembleName() &&
resampleFrequency
),
};
}),
});
}
14 changes: 14 additions & 0 deletions frontend/src/modules/SimulationTimeSeriesMatrix/registerModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ModuleRegistry } from "@framework/ModuleRegistry";

// import { SyncSettingKey } from "@framework/SyncSettings";
// import { broadcastChannelsDef } from "./channelDefs";
import { preview } from "./preview";
import { State } from "./state";

ModuleRegistry.registerModule<State>({
moduleName: "SimulationTimeSeriesMatrix",
defaultTitle: "Simulation Time Series Matrix",
// syncableSettingKeys: [SyncSettingKey.ENSEMBLE, SyncSettingKey.TIME_SERIES],
// broadcastChannelsDef,
preview,
});
Loading

0 comments on commit e0419b3

Please sign in to comment.