From 4f358f7af5d0a6f40c24e7d1f99c7c9346f153d4 Mon Sep 17 00:00:00 2001 From: Sigurd Pettersen Date: Fri, 1 Sep 2023 15:27:20 +0200 Subject: [PATCH] Consistent client-side handling of timestamps (#244) --- .../primary/routers/correlations/router.py | 98 ++++---- .../primary/routers/timeseries/converters.py | 2 +- .../primary/routers/timeseries/router.py | 28 ++- .../primary/routers/timeseries/schemas.py | 8 +- .../src/services/summary_vector_statistics.py | 5 +- .../services/sumo_access/summary_access.py | 20 +- .../src/services/sumo_access/summary_types.py | 5 +- frontend/src/api/ApiService.ts | 3 - frontend/src/api/index.ts | 2 - .../src/api/models/EnsembleCorrelations.ts | 9 - .../src/api/models/VectorHistoricalData.ts | 2 +- .../src/api/models/VectorRealizationData.ts | 2 +- .../src/api/models/VectorStatisticData.ts | 2 +- .../models/VectorStatisticSensitivityData.ts | 2 +- .../src/api/services/CorrelationsService.ts | 75 ------ .../src/api/services/TimeseriesService.ts | 25 +- frontend/src/framework/WorkbenchServices.ts | 10 +- .../src/framework/utils/timestampUtils.ts | 61 +++++ .../DbgWorkbenchSpy/implementation.tsx | 17 +- .../src/modules/DistributionPlot/view.tsx | 4 +- .../sensitivityResponseCalculator.ts | 12 +- frontend/src/modules/Sensitivity/view.tsx | 68 ++++-- .../src/modules/SimulationTimeSeries/view.tsx | 69 +++--- .../simulationTimeSeriesChart/chart.tsx | 147 +++++++----- .../simulationTimeSeriesChart/traces.ts | 26 +-- .../SimulationTimeSeriesSensitivity/view.tsx | 220 +++++++++++------- .../loadModule.tsx | 2 +- .../queryHooks.tsx | 101 ++------ .../settings.tsx | 25 +- .../TimeSeriesParameterDistribution/state.ts | 2 +- .../TimeSeriesParameterDistribution/view.tsx | 12 +- .../tests/unit-tests/timestampUtils.test.ts | 64 +++++ 32 files changed, 603 insertions(+), 525 deletions(-) delete mode 100644 frontend/src/api/models/EnsembleCorrelations.ts delete mode 100644 frontend/src/api/services/CorrelationsService.ts create mode 100644 frontend/src/framework/utils/timestampUtils.ts create mode 100644 frontend/tests/unit-tests/timestampUtils.test.ts diff --git a/backend/src/backend/primary/routers/correlations/router.py b/backend/src/backend/primary/routers/correlations/router.py index ed60ad4cc..6d7404311 100644 --- a/backend/src/backend/primary/routers/correlations/router.py +++ b/backend/src/backend/primary/routers/correlations/router.py @@ -18,59 +18,59 @@ router = APIRouter() -@router.get("/correlate_parameters_with_timeseries/") -def correlate_parameters_with_timeseries( - # fmt:off - authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), - ensemble_name: str = Query(description="Ensemble name"), - vector_name: str = Query(description="Name of the vector"), - timestep: datetime.datetime = Query(description= "Timestep"), - # realizations: Optional[Sequence[int]] = Query(None, description="Optional list of realizations to include. If not specified, all realizations will be returned."), - # parameter_names: Optional[List[str]] = Query(None, description="Optional subset of parameters to correlate. Default are all parameters.") - # fmt:on -) -> EnsembleCorrelations: - """Get parameter correlations for a timeseries at a given timestep""" +# @router.get("/correlate_parameters_with_timeseries/") +# def correlate_parameters_with_timeseries( +# # fmt:off +# authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), +# case_uuid: str = Query(description="Sumo case uuid"), +# ensemble_name: str = Query(description="Ensemble name"), +# vector_name: str = Query(description="Name of the vector"), +# timestep: datetime.datetime = Query(description= "Timestep"), +# # realizations: Optional[Sequence[int]] = Query(None, description="Optional list of realizations to include. If not specified, all realizations will be returned."), +# # parameter_names: Optional[List[str]] = Query(None, description="Optional subset of parameters to correlate. Default are all parameters.") +# # fmt:on +# ) -> EnsembleCorrelations: +# """Get parameter correlations for a timeseries at a given timestep""" - summary_access = SummaryAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - parameter_access = ParameterAccess( - authenticated_user.get_sumo_access_token(), - case_uuid=case_uuid, - iteration_name=ensemble_name, - ) +# summary_access = SummaryAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) +# parameter_access = ParameterAccess( +# authenticated_user.get_sumo_access_token(), +# case_uuid=case_uuid, +# iteration_name=ensemble_name, +# ) - ensemble_response = summary_access.get_vector_values_at_timestep( - vector_name=vector_name, timestep=timestep, realizations=None - ) - parameters = parameter_access.get_parameters_and_sensitivities() +# ensemble_response = summary_access.get_vector_values_at_timestep( +# vector_name=vector_name, timestep=timestep, realizations=None +# ) +# parameters = parameter_access.get_parameters_and_sensitivities() - return correlate_parameters_with_response(parameters.parameters, ensemble_response) +# return correlate_parameters_with_response(parameters.parameters, ensemble_response) -@router.get("/correlate_parameters_with_inplace_volumes/") -def correlate_parameters_with_inplace_volumes( - # fmt:off - authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), - ensemble_name: str = Query(description="Ensemble name"), - table_name: str = Query(description="Table name"), - response_name:str = Query(description="Response name"), - # categorical_filter:Optional[List[InplaceVolumetricsCategoricalMetaData]] = None, - # realizations: Optional[Sequence[int]] = Query(None, description="Optional list of realizations to include. If not specified, all realizations will be returned."), - # parameter_names: Optional[List[str]] = Query(None, description="Optional subset of parameters to correlate. Default are all parameters.") - # fmt:on -) -> EnsembleCorrelations: - """Get parameter correlations for an inplace volumetrics response""" +# @router.get("/correlate_parameters_with_inplace_volumes/") +# def correlate_parameters_with_inplace_volumes( +# # fmt:off +# authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), +# case_uuid: str = Query(description="Sumo case uuid"), +# ensemble_name: str = Query(description="Ensemble name"), +# table_name: str = Query(description="Table name"), +# response_name:str = Query(description="Response name"), +# # categorical_filter:Optional[List[InplaceVolumetricsCategoricalMetaData]] = None, +# # realizations: Optional[Sequence[int]] = Query(None, description="Optional list of realizations to include. If not specified, all realizations will be returned."), +# # parameter_names: Optional[List[str]] = Query(None, description="Optional subset of parameters to correlate. Default are all parameters.") +# # fmt:on +# ) -> EnsembleCorrelations: +# """Get parameter correlations for an inplace volumetrics response""" - inplace_access = InplaceVolumetricsAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - parameter_access = ParameterAccess( - authenticated_user.get_sumo_access_token(), - case_uuid=case_uuid, - iteration_name=ensemble_name, - ) - ensemble_response = inplace_access.get_response( - table_name, response_name, categorical_filters=None, realizations=None - ) - parameters = parameter_access.get_parameters_and_sensitivities() +# inplace_access = InplaceVolumetricsAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) +# parameter_access = ParameterAccess( +# authenticated_user.get_sumo_access_token(), +# case_uuid=case_uuid, +# iteration_name=ensemble_name, +# ) +# ensemble_response = inplace_access.get_response( +# table_name, response_name, categorical_filters=None, realizations=None +# ) +# parameters = parameter_access.get_parameters_and_sensitivities() - return correlate_parameters_with_response(parameters.parameters, ensemble_response) +# return correlate_parameters_with_response(parameters.parameters, ensemble_response) diff --git a/backend/src/backend/primary/routers/timeseries/converters.py b/backend/src/backend/primary/routers/timeseries/converters.py index 151fd84a0..da715f8e2 100644 --- a/backend/src/backend/primary/routers/timeseries/converters.py +++ b/backend/src/backend/primary/routers/timeseries/converters.py @@ -41,7 +41,7 @@ def to_api_vector_statistic_data( ret_data = schemas.VectorStatisticData( realizations=vector_statistics.realizations, - timestamps=vector_statistics.timestamps, + timestamps_utc_ms=vector_statistics.timestamps_utc_ms, value_objects=value_objects, unit=vector_metadata.unit, is_rate=vector_metadata.is_rate, diff --git a/backend/src/backend/primary/routers/timeseries/router.py b/backend/src/backend/primary/routers/timeseries/router.py index afe3f023a..a1a0a652f 100644 --- a/backend/src/backend/primary/routers/timeseries/router.py +++ b/backend/src/backend/primary/routers/timeseries/router.py @@ -67,7 +67,7 @@ def get_realizations_vector_data( ret_arr.append( schemas.VectorRealizationData( realization=vec.realization, - timestamps=vec.timestamps, + timestamps_utc_ms=vec.timestamps_utc_ms, values=vec.values, unit=vec.metadata.unit, is_rate=vec.metadata.is_rate, @@ -77,15 +77,15 @@ def get_realizations_vector_data( return ret_arr -@router.get("/timesteps/") -def get_timesteps( +@router.get("/timestamps_list/") +def get_timestamps_list( authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), case_uuid: str = Query(description="Sumo case uuid"), ensemble_name: str = Query(description="Ensemble name"), resampling_frequency: Optional[schemas.Frequency] = Query(None, description="Resampling frequency"), # realizations: Union[Sequence[int], None] = Query(None, description="Optional list of realizations to include"), -) -> List[datetime.datetime]: - """Get the intersection of available timesteps. +) -> List[int]: + """Get the intersection of available timestamps. Note that when resampling_frequency is None, the pure intersection of the stored raw dates will be returned. Thus the returned list of dates will not include dates from long running realizations. @@ -94,7 +94,7 @@ def get_timesteps( """ access = SummaryAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) sumo_freq = Frequency.from_string_value(resampling_frequency.value if resampling_frequency else "dummy") - return access.get_timesteps(resampling_frequency=sumo_freq) + return access.get_timestamps(resampling_frequency=sumo_freq) @router.get("/historical_vector_data/") @@ -118,7 +118,7 @@ def get_historical_vector_data( raise HTTPException(status_code=404, detail="Could not get historical vector") return schemas.VectorHistoricalData( - timestamps=sumo_hist_vec.timestamps, + timestamps_utc_ms=sumo_hist_vec.timestamps_utc_ms, values=sumo_hist_vec.values, unit=sumo_hist_vec.metadata.unit, is_rate=sumo_hist_vec.metadata.is_rate, @@ -202,7 +202,7 @@ def get_statistical_vector_data_per_sensitivity( sensitivity_name=sensitivity.name, sensitivity_case=case.name, realizations=statistic_data.realizations, - timestamps=statistic_data.timestamps, + timestamps_utc_ms=statistic_data.timestamps_utc_ms, value_objects=statistic_data.value_objects, unit=statistic_data.unit, is_rate=statistic_data.is_rate, @@ -211,22 +211,20 @@ def get_statistical_vector_data_per_sensitivity( return ret_data -@router.get("/realization_vector_at_timestep/") -def get_realization_vector_at_timestep( +@router.get("/realization_vector_at_timestamp/") +def get_realization_vector_at_timestamp( # fmt:off authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), case_uuid: str = Query(description="Sumo case uuid"), ensemble_name: str = Query(description="Ensemble name"), vector_name: str = Query(description="Name of the vector"), - timestep: datetime.datetime = Query(description= "Timestep"), + timestamp_utc_ms: int = Query(description= "Timestamp in ms UTC to query vectors at"), # realizations: Optional[Sequence[int]] = Query(None, description="Optional list of realizations to include. If not specified, all realizations will be returned."), # fmt:on ) -> EnsembleScalarResponse: - """Get parameter correlations for a timeseries at a given timestep""" - summary_access = SummaryAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - ensemble_response = summary_access.get_vector_values_at_timestep( - vector_name=vector_name, timestep=timestep, realizations=None + ensemble_response = summary_access.get_vector_values_at_timestamp( + vector_name=vector_name, timestamp_utc_ms=timestamp_utc_ms, realizations=None ) return ensemble_response diff --git a/backend/src/backend/primary/routers/timeseries/schemas.py b/backend/src/backend/primary/routers/timeseries/schemas.py index 28465a527..500a7a75c 100644 --- a/backend/src/backend/primary/routers/timeseries/schemas.py +++ b/backend/src/backend/primary/routers/timeseries/schemas.py @@ -29,7 +29,7 @@ class VectorDescription(BaseModel): class VectorHistoricalData(BaseModel): - timestamps: List[datetime.datetime] + timestamps_utc_ms: List[int] values: List[float] unit: str is_rate: bool @@ -37,7 +37,7 @@ class VectorHistoricalData(BaseModel): class VectorRealizationData(BaseModel): realization: int - timestamps: List[datetime.datetime] + timestamps_utc_ms: List[int] values: List[float] unit: str is_rate: bool @@ -50,7 +50,7 @@ class StatisticValueObject(BaseModel): class VectorStatisticData(BaseModel): realizations: List[int] - timestamps: List[datetime.datetime] + timestamps_utc_ms: List[int] value_objects: List[StatisticValueObject] unit: str is_rate: bool @@ -58,7 +58,7 @@ class VectorStatisticData(BaseModel): class VectorStatisticSensitivityData(BaseModel): realizations: List[int] - timestamps: List[datetime.datetime] + timestamps_utc_ms: List[int] value_objects: List[StatisticValueObject] unit: str is_rate: bool diff --git a/backend/src/services/summary_vector_statistics.py b/backend/src/services/summary_vector_statistics.py index 38b817504..d2b088710 100644 --- a/backend/src/services/summary_vector_statistics.py +++ b/backend/src/services/summary_vector_statistics.py @@ -1,4 +1,3 @@ -import datetime from typing import Dict, List, Optional, Sequence import numpy as np @@ -15,7 +14,7 @@ class VectorStatistics(BaseModel): realizations: List[int] - timestamps: List[datetime.datetime] + timestamps_utc_ms: List[int] values_dict: Dict[StatisticFunction, List[float]] @@ -106,7 +105,7 @@ def compute_vector_statistics( ret_data = VectorStatistics( realizations=unique_realizations, - timestamps=statistics_table["DATE"].to_numpy().astype(datetime.datetime).tolist(), + timestamps_utc_ms=statistics_table["DATE"].to_numpy().astype(int).tolist(), values_dict=values_dict, ) diff --git a/backend/src/services/sumo_access/summary_access.py b/backend/src/services/sumo_access/summary_access.py index ac89b1e0c..e1fb859c7 100644 --- a/backend/src/services/sumo_access/summary_access.py +++ b/backend/src/services/sumo_access/summary_access.py @@ -1,4 +1,3 @@ -import datetime import logging from io import BytesIO from typing import List, Optional, Sequence, Tuple, Set @@ -143,7 +142,7 @@ def get_vector( ret_arr.append( RealizationVector( realization=real, - timestamps=date_np_arr.astype(datetime.datetime).tolist(), + timestamps_utc_ms=date_np_arr.astype(int).tolist(), values=value_np_arr.tolist(), metadata=vector_metadata, ) @@ -199,15 +198,15 @@ def get_matching_historical_vector( ) return HistoricalVector( - timestamps=date_np_arr.astype(datetime.datetime).tolist(), + timestamps_utc_ms=date_np_arr.astype(int).tolist(), values=value_np_arr.tolist(), metadata=vector_metadata, ) - def get_vector_values_at_timestep( + def get_vector_values_at_timestamp( self, vector_name: str, - timestep: datetime.datetime, + timestamp_utc_ms: int, realizations: Optional[Sequence[int]] = None, ) -> EnsembleScalarResponse: table, _ = self.get_vector_table(vector_name, resampling_frequency=None, realizations=realizations) @@ -215,7 +214,7 @@ def get_vector_values_at_timestep( if realizations is not None: mask = pc.is_in(table["REAL"], value_set=pa.array(realizations)) table = table.filter(mask) - mask = pc.is_in(table["DATE"], value_set=pa.array([timestep])) + mask = pc.is_in(table["DATE"], value_set=pa.array([timestamp_utc_ms])) table = table.filter(mask) return EnsembleScalarResponse( @@ -223,17 +222,20 @@ def get_vector_values_at_timestep( values=table[vector_name].to_pylist(), ) - def get_timesteps( + def get_timestamps( self, resampling_frequency: Optional[Frequency] = None, - ) -> List[datetime.datetime]: + ) -> List[int]: + """ + Get list of available timestamps in ms UTC + """ table, _ = self.get_vector_table( self.get_available_vectors()[0].name, resampling_frequency=resampling_frequency, realizations=None, ) - return pc.unique(table.column("DATE")).to_pylist() + return pc.unique(table.column("DATE")).to_numpy().astype(int).tolist() def _load_arrow_table_for_from_sumo(case: Case, iteration_name: str, vector_name: str) -> Optional[pa.Table]: diff --git a/backend/src/services/sumo_access/summary_types.py b/backend/src/services/sumo_access/summary_types.py index de52ac218..40c30d4d6 100644 --- a/backend/src/services/sumo_access/summary_types.py +++ b/backend/src/services/sumo_access/summary_types.py @@ -1,4 +1,3 @@ -import datetime from enum import Enum from typing import List, Optional @@ -36,12 +35,12 @@ class VectorMetadata(BaseModel): class RealizationVector(BaseModel): realization: int - timestamps: List[datetime.datetime] + timestamps_utc_ms: List[int] values: List[float] metadata: VectorMetadata class HistoricalVector(BaseModel): - timestamps: List[datetime.datetime] + timestamps_utc_ms: List[int] values: List[float] metadata: VectorMetadata diff --git a/frontend/src/api/ApiService.ts b/frontend/src/api/ApiService.ts index 195e813de..44c7fe0f9 100644 --- a/frontend/src/api/ApiService.ts +++ b/frontend/src/api/ApiService.ts @@ -5,7 +5,6 @@ import type { BaseHttpRequest } from './core/BaseHttpRequest'; import type { OpenAPIConfig } from './core/OpenAPI'; import { AxiosHttpRequest } from './core/AxiosHttpRequest'; -import { CorrelationsService } from './services/CorrelationsService'; import { DefaultService } from './services/DefaultService'; import { ExploreService } from './services/ExploreService'; import { GridService } from './services/GridService'; @@ -22,7 +21,6 @@ type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest; export class ApiService { - public readonly correlations: CorrelationsService; public readonly default: DefaultService; public readonly explore: ExploreService; public readonly grid: GridService; @@ -50,7 +48,6 @@ export class ApiService { ENCODE_PATH: config?.ENCODE_PATH, }); - this.correlations = new CorrelationsService(this.request); this.default = new DefaultService(this.request); this.explore = new ExploreService(this.request); this.grid = new GridService(this.request); diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 9c9d5d0a2..1923f75da 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -14,7 +14,6 @@ export type { Body_get_realizations_response as Body_get_realizations_response_a export type { CaseInfo as CaseInfo_api } from './models/CaseInfo'; export type { Completions as Completions_api } from './models/Completions'; export type { DynamicSurfaceDirectory as DynamicSurfaceDirectory_api } from './models/DynamicSurfaceDirectory'; -export type { EnsembleCorrelations as EnsembleCorrelations_api } from './models/EnsembleCorrelations'; export type { EnsembleDetails as EnsembleDetails_api } from './models/EnsembleDetails'; export type { EnsembleInfo as EnsembleInfo_api } from './models/EnsembleInfo'; export type { EnsembleParameter as EnsembleParameter_api } from './models/EnsembleParameter'; @@ -55,7 +54,6 @@ export type { WellCompletionUnits as WellCompletionUnits_api } from './models/We export type { WellCompletionWell as WellCompletionWell_api } from './models/WellCompletionWell'; export type { WellCompletionZone as WellCompletionZone_api } from './models/WellCompletionZone'; -export { CorrelationsService } from './services/CorrelationsService'; export { DefaultService } from './services/DefaultService'; export { ExploreService } from './services/ExploreService'; export { GridService } from './services/GridService'; diff --git a/frontend/src/api/models/EnsembleCorrelations.ts b/frontend/src/api/models/EnsembleCorrelations.ts deleted file mode 100644 index 093dba128..000000000 --- a/frontend/src/api/models/EnsembleCorrelations.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -export type EnsembleCorrelations = { - names: Array; - values: Array; -}; - diff --git a/frontend/src/api/models/VectorHistoricalData.ts b/frontend/src/api/models/VectorHistoricalData.ts index abe433b44..4810748ca 100644 --- a/frontend/src/api/models/VectorHistoricalData.ts +++ b/frontend/src/api/models/VectorHistoricalData.ts @@ -3,7 +3,7 @@ /* eslint-disable */ export type VectorHistoricalData = { - timestamps: Array; + timestamps_utc_ms: Array; values: Array; unit: string; is_rate: boolean; diff --git a/frontend/src/api/models/VectorRealizationData.ts b/frontend/src/api/models/VectorRealizationData.ts index 0d3f3b4e5..60bf95ee5 100644 --- a/frontend/src/api/models/VectorRealizationData.ts +++ b/frontend/src/api/models/VectorRealizationData.ts @@ -4,7 +4,7 @@ export type VectorRealizationData = { realization: number; - timestamps: Array; + timestamps_utc_ms: Array; values: Array; unit: string; is_rate: boolean; diff --git a/frontend/src/api/models/VectorStatisticData.ts b/frontend/src/api/models/VectorStatisticData.ts index f923193cf..90cf7f977 100644 --- a/frontend/src/api/models/VectorStatisticData.ts +++ b/frontend/src/api/models/VectorStatisticData.ts @@ -6,7 +6,7 @@ import type { StatisticValueObject } from './StatisticValueObject'; export type VectorStatisticData = { realizations: Array; - timestamps: Array; + timestamps_utc_ms: Array; value_objects: Array; unit: string; is_rate: boolean; diff --git a/frontend/src/api/models/VectorStatisticSensitivityData.ts b/frontend/src/api/models/VectorStatisticSensitivityData.ts index b9c3985bd..bb73aecb0 100644 --- a/frontend/src/api/models/VectorStatisticSensitivityData.ts +++ b/frontend/src/api/models/VectorStatisticSensitivityData.ts @@ -6,7 +6,7 @@ import type { StatisticValueObject } from './StatisticValueObject'; export type VectorStatisticSensitivityData = { realizations: Array; - timestamps: Array; + timestamps_utc_ms: Array; value_objects: Array; unit: string; is_rate: boolean; diff --git a/frontend/src/api/services/CorrelationsService.ts b/frontend/src/api/services/CorrelationsService.ts deleted file mode 100644 index 4f5fb6240..000000000 --- a/frontend/src/api/services/CorrelationsService.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { EnsembleCorrelations } from '../models/EnsembleCorrelations'; - -import type { CancelablePromise } from '../core/CancelablePromise'; -import type { BaseHttpRequest } from '../core/BaseHttpRequest'; - -export class CorrelationsService { - - constructor(public readonly httpRequest: BaseHttpRequest) {} - - /** - * Correlate Parameters With Timeseries - * Get parameter correlations for a timeseries at a given timestep - * @param caseUuid Sumo case uuid - * @param ensembleName Ensemble name - * @param vectorName Name of the vector - * @param timestep Timestep - * @returns EnsembleCorrelations Successful Response - * @throws ApiError - */ - public correlateParametersWithTimeseries( - caseUuid: string, - ensembleName: string, - vectorName: string, - timestep: string, - ): CancelablePromise { - return this.httpRequest.request({ - method: 'GET', - url: '/correlations/correlate_parameters_with_timeseries/', - query: { - 'case_uuid': caseUuid, - 'ensemble_name': ensembleName, - 'vector_name': vectorName, - 'timestep': timestep, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * Correlate Parameters With Inplace Volumes - * Get parameter correlations for an inplace volumetrics response - * @param caseUuid Sumo case uuid - * @param ensembleName Ensemble name - * @param tableName Table name - * @param responseName Response name - * @returns EnsembleCorrelations Successful Response - * @throws ApiError - */ - public correlateParametersWithInplaceVolumes( - caseUuid: string, - ensembleName: string, - tableName: string, - responseName: string, - ): CancelablePromise { - return this.httpRequest.request({ - method: 'GET', - url: '/correlations/correlate_parameters_with_inplace_volumes/', - query: { - 'case_uuid': caseUuid, - 'ensemble_name': ensembleName, - 'table_name': tableName, - 'response_name': responseName, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - -} diff --git a/frontend/src/api/services/TimeseriesService.ts b/frontend/src/api/services/TimeseriesService.ts index 18bb37e57..5db9ad2bd 100644 --- a/frontend/src/api/services/TimeseriesService.ts +++ b/frontend/src/api/services/TimeseriesService.ts @@ -77,8 +77,8 @@ export class TimeseriesService { } /** - * Get Timesteps - * Get the intersection of available timesteps. + * Get Timestamps List + * Get the intersection of available timestamps. * Note that when resampling_frequency is None, the pure intersection of the * stored raw dates will be returned. Thus the returned list of dates will not include * dates from long running realizations. @@ -87,17 +87,17 @@ export class TimeseriesService { * @param caseUuid Sumo case uuid * @param ensembleName Ensemble name * @param resamplingFrequency Resampling frequency - * @returns string Successful Response + * @returns number Successful Response * @throws ApiError */ - public getTimesteps( + public getTimestampsList( caseUuid: string, ensembleName: string, resamplingFrequency?: Frequency, - ): CancelablePromise> { + ): CancelablePromise> { return this.httpRequest.request({ method: 'GET', - url: '/timeseries/timesteps/', + url: '/timeseries/timestamps_list/', query: { 'case_uuid': caseUuid, 'ensemble_name': ensembleName, @@ -211,29 +211,28 @@ export class TimeseriesService { } /** - * Get Realization Vector At Timestep - * Get parameter correlations for a timeseries at a given timestep + * Get Realization Vector At Timestamp * @param caseUuid Sumo case uuid * @param ensembleName Ensemble name * @param vectorName Name of the vector - * @param timestep Timestep + * @param timestampUtcMs Timestamp in ms UTC to query vectors at * @returns EnsembleScalarResponse Successful Response * @throws ApiError */ - public getRealizationVectorAtTimestep( + public getRealizationVectorAtTimestamp( caseUuid: string, ensembleName: string, vectorName: string, - timestep: string, + timestampUtcMs: number, ): CancelablePromise { return this.httpRequest.request({ method: 'GET', - url: '/timeseries/realization_vector_at_timestep/', + url: '/timeseries/realization_vector_at_timestamp/', query: { 'case_uuid': caseUuid, 'ensemble_name': ensembleName, 'vector_name': vectorName, - 'timestep': timestep, + 'timestamp_utc_ms': timestampUtcMs, }, errors: { 422: `Validation Error`, diff --git a/frontend/src/framework/WorkbenchServices.ts b/frontend/src/framework/WorkbenchServices.ts index 11c6d7d9d..aceee5246 100644 --- a/frontend/src/framework/WorkbenchServices.ts +++ b/frontend/src/framework/WorkbenchServices.ts @@ -13,8 +13,8 @@ export type NavigatorTopicDefinitions = { export type GlobalTopicDefinitions = { "global.infoMessage": string; - "global.hoverRealization": { realization: number }; - "global.hoverTimestamp": { timestamp: number }; + "global.hoverRealization": { realization: number } | null; + "global.hoverTimestamp": { timestampUtcMs: number } | null; "global.syncValue.ensembles": EnsembleIdent[]; "global.syncValue.date": { timeOrInterval: string }; @@ -37,7 +37,7 @@ export type TopicDefinitionsType = T extend ? NavigatorTopicDefinitions[T] : never; -export type CallbackFunction = (value: AllTopicDefinitions[T]) => void; +export type CallbackFunction = (value: AllTopicDefinitions[T] | null) => void; export class WorkbenchServices { protected _workbench: Workbench; @@ -104,7 +104,7 @@ export function useSubscribedValue( React.useEffect( function subscribeToServiceTopic() { - function handleNewValue(newValue: AllTopicDefinitions[T]) { + function handleNewValue(newValue: AllTopicDefinitions[T] | null) { setLatestValue(newValue); } const unsubscribeFunc = workbenchServices.subscribe(topic, handleNewValue); @@ -130,7 +130,7 @@ export function useSubscribedValueConditionally) { //----------------------------------------------------------------------------------------------------------- export function WorkbenchSpyView(props: ModuleFCProps) { const ensembleSet = useEnsembleSet(props.workbenchSession); - const [hoverRealization, hoverRealization_TS] = useServiceValueWithTS("global.hoverRealization", props.workbenchServices); + const [hoverRealization, hoverRealization_TS] = useServiceValueWithTS( + "global.hoverRealization", + props.workbenchServices + ); const [hoverTimestamp, hoverTimestamp_TS] = useServiceValueWithTS("global.hoverTimestamp", props.workbenchServices); const triggeredRefreshCounter = props.moduleContext.useStoreValue("triggeredRefreshCounter"); @@ -43,7 +47,8 @@ export function WorkbenchSpyView(props: ModuleFCProps) { {makeTableRow("hoverRealization", hoverRealization?.realization, hoverRealization_TS)} - {makeTableRow("hoverTimestamp", hoverTimestamp?.timestamp, hoverTimestamp_TS)} + {makeTableRow("hoverTimestamp", hoverTimestamp?.timestampUtcMs, hoverTimestamp_TS)} + {makeTableRow("hoverTimestamp isoStr", hoverTimestamp ? timestampUtcMsToIsoString(hoverTimestamp.timestampUtcMs) : "UNDEF")}

@@ -57,14 +62,14 @@ export function WorkbenchSpyView(props: ModuleFCProps) { ); } -function makeTableRow(label: string, value: any, ts: string) { +function makeTableRow(label: string, value: any, updatedTS?: string) { return ( {label} {value || "N/A"} - ({ts}) + {updatedTS ? `(${updatedTS})` : null} ); } @@ -105,7 +110,7 @@ function useServiceValueWithTS( React.useEffect( function subscribeToServiceTopic() { - function handleNewValue(newValue: AllTopicDefinitions[T]) { + function handleNewValue(newValue: AllTopicDefinitions[T] | null) { setLatestValue(newValue); setLastUpdatedTS(getTimestampString()); } diff --git a/frontend/src/modules/DistributionPlot/view.tsx b/frontend/src/modules/DistributionPlot/view.tsx index a5c523f95..aaf1f8f8d 100644 --- a/frontend/src/modules/DistributionPlot/view.tsx +++ b/frontend/src/modules/DistributionPlot/view.tsx @@ -114,9 +114,7 @@ export const view = ({ moduleContext, workbenchServices, workbenchSettings }: Mo React.useEffect(() => { if (channelX?.getDataDef().key === BroadcastChannelKeyCategory.Realization) { workbenchServices.subscribe("global.hoverRealization", (data) => { - if (data.realization !== undefined) { - setHighlightedKey(data.realization); - } + setHighlightedKey(data ? data.realization : null); }); } }, [channelX, workbenchServices]); diff --git a/frontend/src/modules/Sensitivity/sensitivityResponseCalculator.ts b/frontend/src/modules/Sensitivity/sensitivityResponseCalculator.ts index d63719720..675d9736a 100644 --- a/frontend/src/modules/Sensitivity/sensitivityResponseCalculator.ts +++ b/frontend/src/modules/Sensitivity/sensitivityResponseCalculator.ts @@ -1,7 +1,13 @@ -import { EnsembleScalarResponse_api } from "@api"; import { EnsembleSensitivities, Sensitivity, SensitivityCase, SensitivityType } from "@framework/EnsembleSensitivities"; import { computeQuantile } from "@modules_shared/statistics"; +export type EnsembleScalarResponse = { + realizations: number[]; + values: number[]; + name?: string; + unit?: string; +}; + export interface SensitivityResponse { sensitivityName: string; lowCaseName: string; @@ -35,14 +41,14 @@ export class SensitivityResponseCalculator { /** * Class for calculating sensitivities for a given Ensemble response */ - private _ensembleResponse: EnsembleScalarResponse_api; + private _ensembleResponse: EnsembleScalarResponse; private _sensitivities: EnsembleSensitivities; private _referenceSensitivity: string; private _referenceAverage: number; constructor( sensitivities: EnsembleSensitivities, - ensembleResponse: EnsembleScalarResponse_api, + ensembleResponse: EnsembleScalarResponse, referenceSensitivity = "rms_seed" ) { this._ensembleResponse = ensembleResponse; diff --git a/frontend/src/modules/Sensitivity/view.tsx b/frontend/src/modules/Sensitivity/view.tsx index d6d7bd927..1dd06118c 100644 --- a/frontend/src/modules/Sensitivity/view.tsx +++ b/frontend/src/modules/Sensitivity/view.tsx @@ -1,23 +1,31 @@ import React from "react"; -import { BroadcastChannelMeta } from "@framework/Broadcaster"; +import { BroadcastChannelData, BroadcastChannelMeta } from "@framework/Broadcaster"; +import { Ensemble } from "@framework/Ensemble"; import { ModuleFCProps } from "@framework/Module"; -import { useFirstEnsembleInEnsembleSet } from "@framework/WorkbenchSession"; +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 { SensitivityResponseCalculator } from "./sensitivityResponseCalculator"; +import { EnsembleScalarResponse, SensitivityResponseCalculator } from "./sensitivityResponseCalculator"; import SensitivityTable from "./sensitivityTable"; import { PlotType, State } from "./state"; export const view = ({ moduleContext, workbenchSession, workbenchServices }: ModuleFCProps) => { + // Leave this in until we get a feeling for React18/Plotly + const renderCount = React.useRef(0); + React.useEffect(function incrementRenderCount() { + renderCount.current = renderCount.current + 1; + }); + const wrapperDivRef = React.useRef(null); const wrapperDivSize = useElementSize(wrapperDivRef); - const firstEnsemble = useFirstEnsembleInEnsembleSet(workbenchSession); + const ensembleSet = useEnsembleSet(workbenchSession); const [plotType, setPlotType] = moduleContext.useStoreState("plotType"); const responseChannelName = moduleContext.useStoreValue("responseChannelName"); - const [responseData, setResponseData] = React.useState(null); + const [channelEnsemble, setChannelEnsemble] = React.useState(null); + const [channelResponseData, setChannelResponseData] = React.useState(null); const [showLabels, setShowLabels] = React.useState(true); const [hideZeroY, setHideZeroY] = React.useState(false); @@ -38,38 +46,48 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod const responseChannel = workbenchServices.getBroadcaster().getChannel(responseChannelName ?? ""); React.useEffect(() => { if (!responseChannel) { - setResponseData(null); + setChannelEnsemble(null); + setChannelResponseData(null); return; } - const handleChannelXChanged = (data: any, metaData: BroadcastChannelMeta) => { + function handleChannelDataChanged(data: BroadcastChannelData[], metaData: BroadcastChannelMeta) { + if (data.length === 0) { + setChannelEnsemble(null); + setChannelResponseData(null); + return; + } + const realizations: number[] = []; const values: number[] = []; - data.forEach((vec: any) => { - realizations.push(vec.key); - values.push(vec.value); + data.forEach((el) => { + realizations.push(el.key as number); + values.push(el.value as number); }); - setResponseData({ + setChannelEnsemble(ensembleSet.findEnsemble(metaData.ensembleIdent)); + setChannelResponseData({ realizations: realizations, values: values, name: metaData.description, unit: metaData.unit, }); - }; - - const unsubscribeFunc = responseChannel.subscribe(handleChannelXChanged); + } + const unsubscribeFunc = responseChannel.subscribe(handleChannelDataChanged); return unsubscribeFunc; - }, [responseChannel]); + }, [responseChannel, ensembleSet]); // Memoize the computation of sensitivity responses. Should we use useMemo? - const sensitivities = firstEnsemble?.getSensitivities(); + const sensitivities = channelEnsemble?.getSensitivities(); const computedSensitivityResponseDataset = React.useMemo(() => { - if (sensitivities && responseData) { + if (sensitivities && channelResponseData) { // How to handle errors? try { - const sensitivityResponseCalculator = new SensitivityResponseCalculator(sensitivities, responseData); + const sensitivityResponseCalculator = new SensitivityResponseCalculator( + sensitivities, + channelResponseData + ); return sensitivityResponseCalculator.computeSensitivitiesForResponse(); } catch (e) { console.warn(e); @@ -77,12 +95,21 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod } } return null; - }, [sensitivities, responseData]); + }, [sensitivities, channelResponseData]); + + let errMessage = ""; + if (!computedSensitivityResponseDataset) { + if (!responseChannel) { + errMessage = "No channel selected"; + } else { + errMessage = "No valid data to plot"; + } + } return (
{/* // TODO: Remove */} - {!computedSensitivityResponseDataset &&
No channels selected
} + {!computedSensitivityResponseDataset &&
{errMessage}
}
@@ -167,6 +194,7 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod
)}
+
(rc={renderCount.current})
); }; diff --git a/frontend/src/modules/SimulationTimeSeries/view.tsx b/frontend/src/modules/SimulationTimeSeries/view.tsx index de96a2dda..9594591d4 100644 --- a/frontend/src/modules/SimulationTimeSeries/view.tsx +++ b/frontend/src/modules/SimulationTimeSeries/view.tsx @@ -7,7 +7,7 @@ import { ModuleFCProps } from "@framework/Module"; import { useSubscribedValue } from "@framework/WorkbenchServices"; import { useElementSize } from "@lib/hooks/useElementSize"; -import { Layout, PlotData, PlotHoverEvent } from "plotly.js"; +import { Layout, PlotData, PlotDatum, PlotHoverEvent } from "plotly.js"; import { BroadcastChannelNames } from "./channelDefs"; import { useHistoricalVectorDataQuery, useStatisticalVectorDataQuery, useVectorDataQuery } from "./queryHooks"; @@ -21,6 +21,12 @@ interface MyPlotData extends Partial { } export const view = ({ moduleContext, workbenchSession, workbenchServices }: ModuleFCProps) => { + // Leave this in until we get a feeling for React18/Plotly + const renderCount = React.useRef(0); + React.useEffect(function incrementRenderCount() { + renderCount.current = renderCount.current + 1; + }); + const wrapperDivRef = React.useRef(null); const wrapperDivSize = useElementSize(wrapperDivRef); const vectorSpec = moduleContext.useStoreValue("vectorSpec"); @@ -89,35 +95,29 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod [vectorQuery.data, ensemble, vectorSpec, moduleContext] ); - // React.useEffect( - // function subscribeToHoverRealizationTopic() { - // const unsubscribeFunc = workbenchServices.subscribe("global.hoverRealization", ({ realization }) => { - // setHighlightRealization(realization); - // }); - // return unsubscribeFunc; - // }, - // [workbenchServices] - // ); - - const subscribedPlotlyTimeStamp = useSubscribedValue("global.hoverTimestamp", workbenchServices); - const subscribedPlotlyRealization = useSubscribedValue("global.hoverRealization", workbenchServices); - // const highlightedTrace - const handleHover = (e: PlotHoverEvent) => { - if (e.xvals.length > 0 && typeof e.xvals[0]) { - workbenchServices.publishGlobalData("global.hoverTimestamp", { timestamp: e.xvals[0] as number }); + const subscribedHoverTimestamp = useSubscribedValue("global.hoverTimestamp", workbenchServices); + const subscribedHoverRealization = useSubscribedValue("global.hoverRealization", workbenchServices); + + function handleHover(e: PlotHoverEvent) { + const plotDatum: PlotDatum = e.points[0]; + + if (plotDatum.pointIndex >= 0 && plotDatum.pointIndex < plotDatum.data.x.length) { + const timestampUtcMs = plotDatum.data.x[plotDatum.pointIndex]; + if (typeof timestampUtcMs === "number") + workbenchServices.publishGlobalData("global.hoverTimestamp", { timestampUtcMs: timestampUtcMs }); } - const curveData = e.points[0].data as MyPlotData; - if (typeof curveData.realizationNumber === "number") { - // setHighlightRealization(curveData.realizationNumber); + const curveData = plotDatum.data as MyPlotData; + if (typeof curveData.realizationNumber === "number") { workbenchServices.publishGlobalData("global.hoverRealization", { realization: curveData.realizationNumber, }); } - }; + } function handleUnHover() { - workbenchServices.publishGlobalData("global.hoverRealization", { realization: -1 }); + workbenchServices.publishGlobalData("global.hoverRealization", null); + workbenchServices.publishGlobalData("global.hoverTimestamp", null); } const tracesDataArr: MyPlotData[] = []; @@ -126,12 +126,12 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod let highlightedTrace: MyPlotData | null = null; for (let i = 0; i < vectorQuery.data.length; i++) { const vec = vectorQuery.data[i]; - const isHighlighted = vec.realization === subscribedPlotlyRealization?.realization ? true : false; - const curveColor = vec.realization === subscribedPlotlyRealization?.realization ? "red" : "green"; - const lineWidth = vec.realization === subscribedPlotlyRealization?.realization ? 3 : 1; + const isHighlighted = vec.realization === subscribedHoverRealization?.realization ? true : false; + const curveColor = vec.realization === subscribedHoverRealization?.realization ? "red" : "green"; + const lineWidth = vec.realization === subscribedHoverRealization?.realization ? 3 : 1; const lineShape = vec.is_rate ? "vh" : "linear"; const trace: MyPlotData = { - x: vec.timestamps, + x: vec.timestamps_utc_ms, y: vec.values, name: `real-${vec.realization}`, realizationNumber: vec.realization, @@ -157,7 +157,7 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod const lineShape = statisticsQuery.data.is_rate ? "vh" : "linear"; for (const statValueObj of statisticsQuery.data.value_objects) { const trace: MyPlotData = { - x: statisticsQuery.data.timestamps, + x: statisticsQuery.data.timestamps_utc_ms, y: statValueObj.values, name: statValueObj.statistic_function, legendrank: -1, @@ -172,7 +172,7 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod if (showHistorical && historicalQuery.data) { const lineShape = historicalQuery.data.is_rate ? "vh" : "linear"; const trace: MyPlotData = { - x: historicalQuery.data.timestamps, + x: historicalQuery.data.timestamps_utc_ms, y: historicalQuery.data.values, name: "History", legendrank: -1, @@ -210,22 +210,20 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod height: wrapperDivSize.height, title: plotTitle, margin: { t: 30, r: 0, l: 40, b: 40 }, + xaxis: { type: "date" }, }; - if (subscribedPlotlyTimeStamp) { + if (subscribedHoverTimestamp) { layout["shapes"] = [ { type: "line", xref: "x", yref: "paper", - x0: new Date(subscribedPlotlyTimeStamp.timestamp), + x0: subscribedHoverTimestamp.timestampUtcMs, y0: 0, - x1: new Date(subscribedPlotlyTimeStamp.timestamp), + x1: subscribedHoverTimestamp.timestampUtcMs, y1: 1, - line: { - color: "#ccc", - width: 1, - }, + line: { color: "red", width: 1, dash: "dot" }, }, ]; } @@ -239,6 +237,7 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod onHover={handleHover} onUnhover={handleUnHover} /> +
(rc={renderCount.current})
); }; diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/chart.tsx b/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/chart.tsx index 249cfacd4..072fb6d6d 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/chart.tsx +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/chart.tsx @@ -1,89 +1,114 @@ -import React, { useState } from "react"; +import React from "react"; import Plot from "react-plotly.js"; -import { Layout, PlotHoverEvent } from "plotly.js"; +import { Layout, PlotDatum, PlotHoverEvent, PlotMouseEvent } from "plotly.js"; import { TimeSeriesPlotlyTrace } from "./traces"; -export type timeSeriesChartProps = { - traceDataArr: TimeSeriesPlotlyTrace[]; - onHover?: (date: string) => void; +export type HoverInfo = { + timestampUtcMs: number; + realization?: number; + shiftKeyIsDown?: boolean; +}; +export type TimeSeriesChartProps = { + traceDataArr: TimeSeriesPlotlyTrace[]; + activeTimestampUtcMs?: number; + hoveredTimestampUtcMs?: number; + onHover?: (hoverData: HoverInfo | null) => void; + onClick?: (timestampUtcMs: number) => void; height?: number | 100; width?: number | 100; }; -export const TimeSeriesChart: React.FC = (props) => { - const { height, width, traceDataArr } = props; - const [activeTimestamp, setActiveTimestamp] = useState(null); - const [hoverActive, setHoverActive] = useState(true); - const handleClick = () => { - setHoverActive(!hoverActive); - }; - const handleHover = (e: PlotHoverEvent) => { - if (hoverActive && e.xvals.length > 0 && typeof e.xvals[0]) { - // workbenchServices.publishGlobalData("global.hoverTimestamp", { timestamp: e.xvals[0] as number }); - setActiveTimestamp(e.xvals[0] as string); - if (props.onHover) { - props.onHover(e.points[0].x as string); +export const TimeSeriesChart: React.FC = (props) => { + function handleClick(e: PlotMouseEvent) { + const clickedPoint: PlotDatum = e.points[0]; + if (!clickedPoint || !props.onClick) { + return; + } + + if (clickedPoint.pointIndex >= 0 && clickedPoint.pointIndex < clickedPoint.data.x.length) { + const timestampUtcMs = clickedPoint.data.x[clickedPoint.pointIndex]; + if (typeof timestampUtcMs === "number") { + props.onClick(timestampUtcMs); } } - const curveData = e.points[0].data as TimeSeriesPlotlyTrace; - if (typeof curveData.realizationNumber === "number") { - // setHighlightRealization(curveData.realizationNumber); - // workbenchServices.publishGlobalData("global.hoverRealization", { - // realization: curveData.realizationNumber, - // }); + } + + function handleHover(e: PlotHoverEvent) { + const hoveredPoint: PlotDatum = e.points[0]; + if (!hoveredPoint || !props.onHover) { + return; } - }; - const layout: Partial = { - width: width, - height: height, - xaxis: { - type: "category", - }, + if (hoveredPoint.pointIndex >= 0 && hoveredPoint.pointIndex < hoveredPoint.data.x.length) { + const timestampUtcMs = hoveredPoint.data.x[hoveredPoint.pointIndex]; + if (typeof timestampUtcMs === "number") { + let maybeRealizationNumber: number | undefined; + const traceData = hoveredPoint.data as TimeSeriesPlotlyTrace; + if (typeof traceData.realizationNumber === "number") { + maybeRealizationNumber = traceData.realizationNumber; + } + + const hoverInfo: HoverInfo = { + timestampUtcMs: timestampUtcMs, + realization: maybeRealizationNumber, + shiftKeyIsDown: e.event.shiftKey, + }; + props.onHover(hoverInfo); + } + } + } + + function handleUnHover() { + if (props.onHover) { + props.onHover(null); + } + } + + const layout: Partial = { + 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 }, + shapes: [], }; - if (activeTimestamp) { - layout["shapes"] = [ - { - type: "line", - xref: "x", - yref: "paper", - x0: activeTimestamp, - y0: 0, - x1: activeTimestamp, - y1: 1, - line: { - color: "#ccc", - width: 2, - }, - }, - ]; - // This breaks hover... - // layout["annotations"] = [ - // { - // bgcolor: "white", - // showarrow: false, - // text: activeTimestamp, - // x: activeTimestamp, - // y: 1, - // // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // //@ts-ignore - // yref: "y domain", - // }, - // ]; + if (props.activeTimestampUtcMs !== undefined) { + layout.shapes?.push({ + type: "line", + xref: "x", + yref: "paper", + x0: props.activeTimestampUtcMs, + y0: 0, + x1: props.activeTimestampUtcMs, + y1: 1, + line: { color: "#ccc", width: 2 }, + }); } + if (props.hoveredTimestampUtcMs !== undefined) { + layout.shapes?.push({ + type: "line", + xref: "x", + yref: "paper", + x0: props.hoveredTimestampUtcMs, + y0: 0, + x1: props.hoveredTimestampUtcMs, + y1: 1, + line: { color: "red", width: 1, dash: "dot" }, + }); + } + return ( ); }; diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/traces.ts b/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/traces.ts index bc9d9a69d..79485833d 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/traces.ts +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/simulationTimeSeriesChart/traces.ts @@ -7,10 +7,10 @@ export interface TimeSeriesPlotlyTrace extends Partial { legendrank?: number; } -export const createRealizationLineTraces = ( +export function createRealizationLineTraces( realizationData: VectorRealizationData_api[], highlightedRealization?: number | undefined -) => { +): TimeSeriesPlotlyTrace[] { const traces: TimeSeriesPlotlyTrace[] = []; let highlightedTrace: TimeSeriesPlotlyTrace | null = null; realizationData.forEach((vec) => { @@ -18,7 +18,7 @@ export const createRealizationLineTraces = ( const lineWidth = highlightedRealization === vec.realization ? 2 : 1; const lineShape = highlightedRealization === vec.realization ? "spline" : "linear"; const isHighlighted = vec.realization === highlightedRealization ? true : false; - const trace = realizationLineTrace(vec, curveColor, lineWidth, lineShape); + const trace = createSingleRealizationLineTrace(vec, curveColor, lineWidth, lineShape); if (isHighlighted) { highlightedTrace = trace; } else { @@ -29,16 +29,16 @@ export const createRealizationLineTraces = ( traces.push(highlightedTrace); } return traces; -}; +} -const realizationLineTrace = ( +function createSingleRealizationLineTrace( vec: VectorRealizationData_api, curveColor: string, lineWidth: number, lineShape: "linear" | "spline" -): TimeSeriesPlotlyTrace => { +): TimeSeriesPlotlyTrace { return { - x: vec.timestamps, + x: vec.timestamps_utc_ms, y: vec.values, name: `real-${vec.realization}`, realizationNumber: vec.realization, @@ -48,18 +48,18 @@ const realizationLineTrace = ( mode: "lines", line: { color: curveColor, width: lineWidth, shape: lineShape }, }; -}; +} -export const sensitivityStatisticsTrace = ( - timestamps: string[], +export function createSensitivityStatisticsTrace( + timestampsMsUtc: number[], values: number[], name: string, lineShape: "linear" | "spline", lineDash: "dash" | "dot" | "dashdot" | "solid", lineColor?: string | null -): TimeSeriesPlotlyTrace => { +): TimeSeriesPlotlyTrace { return { - x: timestamps, + x: timestampsMsUtc, y: values, name: name, legendrank: -1, @@ -67,4 +67,4 @@ export const sensitivityStatisticsTrace = ( mode: "lines", line: { color: lineColor || "lightblue", width: 4, dash: lineDash, shape: lineShape }, }; -}; +} diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/view.tsx b/frontend/src/modules/SimulationTimeSeriesSensitivity/view.tsx index b3e78457e..a4e965a38 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/view.tsx +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/view.tsx @@ -1,24 +1,29 @@ import React from "react"; -import { VectorRealizationData_api } from "@api"; -import { StatisticFunction_api } from "@api"; -import { BroadcastChannelMeta } from "@framework/Broadcaster"; +import { StatisticFunction_api, VectorRealizationData_api, VectorStatisticSensitivityData_api } from "@api"; +import { BroadcastChannelMeta, BroadcastChannelData } from "@framework/Broadcaster"; +import { Ensemble } from "@framework/Ensemble"; import { ModuleFCProps } from "@framework/Module"; +import { useSubscribedValue } from "@framework/WorkbenchServices"; +import { timestampUtcMsToCompactIsoString } from "@framework/utils/timestampUtils"; import { useElementSize } from "@lib/hooks/useElementSize"; import { indexOf } from "lodash"; import { BroadcastChannelNames } from "./channelDefs"; import { useStatisticalVectorSensitivityDataQuery, useVectorDataQuery } from "./queryHooks"; -import { TimeSeriesChart } from "./simulationTimeSeriesChart/chart"; -import { - TimeSeriesPlotlyTrace, - createRealizationLineTraces, - sensitivityStatisticsTrace, -} from "./simulationTimeSeriesChart/traces"; +import { HoverInfo, TimeSeriesChart } from "./simulationTimeSeriesChart/chart"; +import { TimeSeriesPlotlyTrace } from "./simulationTimeSeriesChart/traces"; +import { createRealizationLineTraces, createSensitivityStatisticsTrace } from "./simulationTimeSeriesChart/traces"; import { State } from "./state"; -export const view = ({ moduleContext, workbenchSession }: ModuleFCProps) => { +export const view = ({ moduleContext, workbenchSession, workbenchServices }: ModuleFCProps) => { + // Leave this in until we get a feeling for React18/Plotly + const renderCount = React.useRef(0); + React.useEffect(function incrementRenderCount() { + renderCount.current = renderCount.current + 1; + }); + const wrapperDivRef = React.useRef(null); const wrapperDivSize = useElementSize(wrapperDivRef); const vectorSpec = moduleContext.useStoreValue("vectorSpec"); @@ -27,8 +32,8 @@ export const view = ({ moduleContext, workbenchSession }: ModuleFCProps) const showRealizations = moduleContext.useStoreValue("showRealizations"); const selectedSensitivity = moduleContext.useStoreValue("selectedSensitivity"); - const [hoveredTimestamp, setHoveredTimestamp] = React.useState(null); - const [traceDataArr, setTraceDataArr] = React.useState([]); + const [activeTimestampUtcMs, setActiveTimestampUtcMs] = React.useState(null); + const subscribedHoverTimestampUtcMs = useSubscribedValue("global.hoverTimestamp", workbenchServices); const realizationsQuery = useVectorDataQuery( vectorSpec?.ensembleIdent.getCaseUuid(), @@ -51,107 +56,148 @@ export const view = ({ moduleContext, workbenchSession }: ModuleFCProps) // Broadcast the data to the realization data channel React.useEffect( function broadcast() { - if (!ensemble) { + if (!ensemble || !realizationsQuery.data || activeTimestampUtcMs === null) { return; } - const dataGenerator = (): { key: number; value: number }[] => { - const data: { key: number; value: number }[] = []; - if (realizationsQuery.data) { - realizationsQuery.data.forEach((vec) => { - const indexOfTimeStamp = indexOf(vec.timestamps, hoveredTimestamp); - data.push({ - key: vec.realization, - value: indexOfTimeStamp === -1 ? 0 : vec.values[indexOfTimeStamp], - }); + const dataGenerator = (): BroadcastChannelData[] => { + const data: BroadcastChannelData[] = []; + realizationsQuery.data.forEach((vec) => { + const indexOfTimeStamp = indexOf(vec.timestamps_utc_ms, activeTimestampUtcMs); + data.push({ + key: vec.realization, + value: indexOfTimeStamp === -1 ? 0 : vec.values[indexOfTimeStamp], }); - } + }); return data; }; + const activeTimestampAsIsoString = timestampUtcMsToCompactIsoString(activeTimestampUtcMs); const channelMeta: BroadcastChannelMeta = { ensembleIdent: ensemble.getIdent(), - description: `${ensemble.getDisplayName()} ${vectorSpec?.vectorName} ${hoveredTimestamp}`, + description: `${ensemble.getDisplayName()} ${vectorSpec?.vectorName} ${activeTimestampAsIsoString}`, unit: realizationsQuery.data?.at(0)?.unit || "", }; moduleContext.getChannel(BroadcastChannelNames.Realization_Value).broadcast(channelMeta, dataGenerator); }, - [realizationsQuery.data, ensemble, vectorSpec, hoveredTimestamp, moduleContext] + [ensemble, vectorSpec, realizationsQuery.data, activeTimestampUtcMs, moduleContext] ); - // Update the Plotly trace data - React.useEffect(() => { - const traceDataArr: TimeSeriesPlotlyTrace[] = []; - if (selectedSensitivity && vectorSpec) { - const ensemble = ensembleSet.findEnsemble(vectorSpec.ensembleIdent); - if (ensemble) { - const sensitivity = ensemble.getSensitivities()?.getSensitivityByName(selectedSensitivity); - if (sensitivity) { - if (statisticsQuery.data) { - const meanCase = statisticsQuery.data.filter((stat) => stat.sensitivity_name === "rms_seed")[0]; - const meanObj = meanCase.value_objects.filter( - (statObj) => statObj.statistic_function === StatisticFunction_api.MEAN - ); - traceDataArr.push( - sensitivityStatisticsTrace( - meanCase.timestamps, - meanObj[0].values, - `reference ${meanCase.sensitivity_name}`, - "linear", - "solid", - "black" - ) - ); - if (showStatistics) { - const cases = statisticsQuery.data.filter( - (stat) => stat.sensitivity_name === selectedSensitivity - ); - if (cases) { - for (const caseIdent of cases) { - const meanObj = caseIdent.value_objects.filter( - (statObj) => statObj.statistic_function === StatisticFunction_api.MEAN - ); - traceDataArr.push( - sensitivityStatisticsTrace( - caseIdent.timestamps, - meanObj[0].values, - caseIdent.sensitivity_case, - "linear", - "dash" - ) - ); - } - } - } - } - if (showRealizations && realizationsQuery.data) { - for (const caseIdent of sensitivity.cases) { - const realsToInclude = caseIdent.realizations; - const realizationData: VectorRealizationData_api[] = realizationsQuery.data.filter((vec) => - realsToInclude.includes(vec.realization) - ); - const traces = createRealizationLineTraces(realizationData); - traceDataArr.push(...traces); - } - } - } + const traceDataArr = React.useMemo(() => { + if (!ensemble || !selectedSensitivity) { + return []; + } + 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) { + if (hoverInfo.shiftKeyIsDown) { + setActiveTimestampUtcMs(hoverInfo.timestampUtcMs); + } + + workbenchServices.publishGlobalData("global.hoverTimestamp", { + timestampUtcMs: hoverInfo.timestampUtcMs, + }); + + if (typeof hoverInfo.realization === "number") { + workbenchServices.publishGlobalData("global.hoverRealization", { + realization: hoverInfo.realization, + }); } + } else { + workbenchServices.publishGlobalData("global.hoverTimestamp", null); + workbenchServices.publishGlobalData("global.hoverRealization", null); } - setTraceDataArr(traceDataArr); - }, [realizationsQuery.data, statisticsQuery.data, showRealizations, showStatistics, selectedSensitivity]); + } - const handleHover = (dateString: string) => { - setHoveredTimestamp(dateString); - }; + function handleClickInChart(timestampUtcMs: number) { + setActiveTimestampUtcMs(timestampUtcMs); + } return (
+
(rc={renderCount.current})
); }; + +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/TimeSeriesParameterDistribution/loadModule.tsx b/frontend/src/modules/TimeSeriesParameterDistribution/loadModule.tsx index afff78d73..fd2a229fb 100644 --- a/frontend/src/modules/TimeSeriesParameterDistribution/loadModule.tsx +++ b/frontend/src/modules/TimeSeriesParameterDistribution/loadModule.tsx @@ -6,7 +6,7 @@ import { view } from "./view"; const defaultState: State = { vectorSpec: null, - timeStep: null, + timestampUtcMs: null, parameterName: undefined }; diff --git a/frontend/src/modules/TimeSeriesParameterDistribution/queryHooks.tsx b/frontend/src/modules/TimeSeriesParameterDistribution/queryHooks.tsx index 273fce5f9..89e5b8285 100644 --- a/frontend/src/modules/TimeSeriesParameterDistribution/queryHooks.tsx +++ b/frontend/src/modules/TimeSeriesParameterDistribution/queryHooks.tsx @@ -1,5 +1,5 @@ -import { Frequency_api, VectorDescription_api } from "@api"; -import { VectorRealizationData_api, EnsembleParameterDescription_api, EnsembleScalarResponse_api, EnsembleParameter_api } from "@api"; +import { EnsembleScalarResponse_api, VectorDescription_api } from "@api"; +import { EnsembleParameterDescription_api, EnsembleParameter_api } from "@api"; import { apiService } from "@framework/ApiService"; import { UseQueryResult, useQuery } from "@tanstack/react-query"; @@ -19,125 +19,62 @@ export function useVectorsQuery( }); } -export function useTimeStepsQuery( +export function useTimestampsListQuery( caseUuid: string | undefined, ensembleName: string | undefined -): UseQueryResult> { +): UseQueryResult> { return useQuery({ - queryKey: ["getTimesteps", caseUuid, ensembleName], - queryFn: () => apiService.timeseries.getTimesteps(caseUuid ?? "", ensembleName ?? ""), + queryKey: ["getTimestampsList", caseUuid, ensembleName], + queryFn: () => apiService.timeseries.getTimestampsList(caseUuid ?? "", ensembleName ?? ""), staleTime: STALE_TIME, cacheTime: CACHE_TIME, enabled: caseUuid && ensembleName ? true : false, }); } -export function useVectorDataQuery( - caseUuid: string | undefined, - ensembleName: string | undefined, - vectorName: string | undefined, - resampleFrequency: Frequency_api | null, - realizationsToInclude: number[] | null -): UseQueryResult> { - return useQuery({ - queryKey: [ - "getRealizationsVectorData", - caseUuid, - ensembleName, - vectorName, - resampleFrequency, - realizationsToInclude, - ], - queryFn: () => - apiService.timeseries.getRealizationsVectorData( - caseUuid ?? "", - ensembleName ?? "", - vectorName ?? "", - resampleFrequency ?? undefined, - realizationsToInclude ?? undefined - ), - staleTime: STALE_TIME, - cacheTime: CACHE_TIME, - enabled: caseUuid && ensembleName && vectorName ? true : false, - }); -} export function useGetParameterNamesQuery( caseUuid: string | undefined, - ensembleName: string | undefined, - + ensembleName: string | undefined ): UseQueryResult { return useQuery({ - queryKey: [ - "getParameterNamesAndDescription", - caseUuid, - ensembleName, - ], - queryFn: () => - apiService.parameters.getParameterNamesAndDescription( - caseUuid ?? "", - ensembleName ?? "", - ), + queryKey: ["getParameterNamesAndDescription", caseUuid, ensembleName], + queryFn: () => apiService.parameters.getParameterNamesAndDescription(caseUuid ?? "", ensembleName ?? ""), staleTime: STALE_TIME, cacheTime: CACHE_TIME, enabled: caseUuid && ensembleName ? true : false, }); } - export function useParameterQuery( caseUuid: string | undefined, ensembleName: string | undefined, - parameterName: string | undefined, - - + parameterName: string | undefined ): UseQueryResult { return useQuery({ - queryKey: [ - "getParameter", - caseUuid, - ensembleName, - parameterName, - - - ], - queryFn: () => - apiService.parameters.getParameter( - caseUuid ?? "", - ensembleName ?? "", - parameterName ?? "", - ), + queryKey: ["getParameter", caseUuid, ensembleName, parameterName], + queryFn: () => apiService.parameters.getParameter(caseUuid ?? "", ensembleName ?? "", parameterName ?? ""), staleTime: STALE_TIME, cacheTime: CACHE_TIME, enabled: caseUuid && ensembleName && parameterName ? true : false, }); } -export function useVectorAtTimestepQuery( +export function useVectorAtTimestampQuery( caseUuid: string | undefined, ensembleName: string | undefined, vectorName: string | undefined, - timeStep: string | null - - + timestampUtcMs: number | null ): UseQueryResult { return useQuery({ - queryKey: [ - "getRealizationVectorAtTimestep", - caseUuid, - ensembleName, - vectorName, - timeStep - - - ], + queryKey: ["getRealizationVectorAtTimestep", caseUuid, ensembleName, vectorName, timestampUtcMs], queryFn: () => - apiService.timeseries.getRealizationVectorAtTimestep( + apiService.timeseries.getRealizationVectorAtTimestamp( caseUuid ?? "", ensembleName ?? "", vectorName ?? "", - timeStep ?? "" + timestampUtcMs ?? 0 ), staleTime: STALE_TIME, cacheTime: CACHE_TIME, - enabled: caseUuid && ensembleName && vectorName && timeStep ? true : false, + enabled: !!(caseUuid && ensembleName && vectorName && timestampUtcMs != null), }); -} \ No newline at end of file +} diff --git a/frontend/src/modules/TimeSeriesParameterDistribution/settings.tsx b/frontend/src/modules/TimeSeriesParameterDistribution/settings.tsx index c8bd04b03..ceebc5aac 100644 --- a/frontend/src/modules/TimeSeriesParameterDistribution/settings.tsx +++ b/frontend/src/modules/TimeSeriesParameterDistribution/settings.tsx @@ -7,13 +7,14 @@ import { SyncSettingKey, SyncSettingsHelper } from "@framework/SyncSettings"; import { useEnsembleSet } from "@framework/WorkbenchSession"; import { SingleEnsembleSelect } from "@framework/components/SingleEnsembleSelect"; import { fixupEnsembleIdent, maybeAssignFirstSyncedEnsemble } from "@framework/utils/ensembleUiHelpers"; +import { timestampUtcMsToCompactIsoString } from "@framework/utils/timestampUtils"; import { ApiStateWrapper } from "@lib/components/ApiStateWrapper"; import { CircularProgress } from "@lib/components/CircularProgress"; import { Dropdown } from "@lib/components/Dropdown"; import { Label } from "@lib/components/Label"; import { Select, SelectOption } from "@lib/components/Select"; -import { useGetParameterNamesQuery, useTimeStepsQuery, useVectorsQuery } from "./queryHooks"; +import { useGetParameterNamesQuery, useTimestampsListQuery, useVectorsQuery } from "./queryHooks"; import { State } from "./state"; //----------------------------------------------------------------------------------------------------------- @@ -22,7 +23,7 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: const [selectedEnsemble, setSelectedEnsemble] = React.useState(null); const [selectedVectorName, setSelectedVectorName] = React.useState(""); - const [timeStep, setTimeStep] = moduleContext.useStoreState("timeStep"); + const [timestampUctMs, setTimestampUtcMs] = moduleContext.useStoreState("timestampUtcMs"); const [parameterName, setParameterName] = moduleContext.useStoreState("parameterName"); const syncedSettingKeys = moduleContext.useSyncedSettingKeys(); @@ -34,7 +35,7 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: const computedEnsemble = fixupEnsembleIdent(candidateEnsemble, ensembleSet); const vectorsQuery = useVectorsQuery(computedEnsemble?.getCaseUuid(), computedEnsemble?.getEnsembleName()); - const timeStepsQuery = useTimeStepsQuery(computedEnsemble?.getCaseUuid(), computedEnsemble?.getEnsembleName()); + const timestampsQuery = useTimestampsListQuery(computedEnsemble?.getCaseUuid(), computedEnsemble?.getEnsembleName()); const parameterNamesQuery = useGetParameterNamesQuery( computedEnsemble?.getCaseUuid(), computedEnsemble?.getEnsembleName() @@ -116,15 +117,15 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: } > @@ -192,11 +193,11 @@ function makeParameterNamesOptionItems(parameters: EnsembleParameterDescription_ return itemArr; } -function makeTimeStepsOptions(timesteps: string[] | undefined): SelectOption[] { +function makeTimeStepsOptions(timestampsUtcMs: number[] | undefined): SelectOption[] { const itemArr: SelectOption[] = []; - if (timesteps) { - for (const timestep of timesteps) { - itemArr.push({ value: timestep, label: timestep }); + if (timestampsUtcMs) { + for (const ts of timestampsUtcMs) { + itemArr.push({ value: `${ts}`, label: timestampUtcMsToCompactIsoString(ts) }); } } return itemArr; diff --git a/frontend/src/modules/TimeSeriesParameterDistribution/state.ts b/frontend/src/modules/TimeSeriesParameterDistribution/state.ts index f64122da0..f52a11052 100644 --- a/frontend/src/modules/TimeSeriesParameterDistribution/state.ts +++ b/frontend/src/modules/TimeSeriesParameterDistribution/state.ts @@ -7,6 +7,6 @@ export interface VectorSpec { export interface State { vectorSpec: VectorSpec | null; - timeStep: string | null; + timestampUtcMs: number | null; parameterName: string | undefined; } diff --git a/frontend/src/modules/TimeSeriesParameterDistribution/view.tsx b/frontend/src/modules/TimeSeriesParameterDistribution/view.tsx index c32824a7d..366527fbd 100644 --- a/frontend/src/modules/TimeSeriesParameterDistribution/view.tsx +++ b/frontend/src/modules/TimeSeriesParameterDistribution/view.tsx @@ -5,7 +5,7 @@ import { useElementSize } from "@lib/hooks/useElementSize"; import PlotlyScatter from "./plotlyScatterChart"; -import { useVectorAtTimestepQuery, useParameterQuery } from "./queryHooks"; +import { useVectorAtTimestampQuery, useParameterQuery } from "./queryHooks"; import { State } from "./state"; @@ -15,13 +15,13 @@ export const view = ({ moduleContext }: ModuleFCProps) => { const vectorSpec = moduleContext.useStoreValue("vectorSpec"); const [highlightedRealization, setHighlightedRealization] = React.useState(-1) const parameterName = moduleContext.useStoreValue("parameterName"); - const timeStep = moduleContext.useStoreValue("timeStep"); + const timestampUtcMs = moduleContext.useStoreValue("timestampUtcMs"); - const vectorAtTimestepQuery = useVectorAtTimestepQuery( + const vectorAtTimestampQuery = useVectorAtTimestampQuery( vectorSpec?.caseUuid, vectorSpec?.ensembleName, vectorSpec?.vectorName, - timeStep + timestampUtcMs ); const parameterQuery = useParameterQuery( @@ -37,10 +37,10 @@ export const view = ({ moduleContext }: ModuleFCProps) => { return (
- {parameterQuery.data && vectorAtTimestepQuery.data && + {parameterQuery.data && vectorAtTimestampQuery.data && { + test("Check if ISO 8601 string contains time", () => { + expect(hasTime("2018-01-01")).toBe(false); + expect(hasTime("2018-01-01T")).toBe(false); + + expect(hasTime("2018-01-01T00")).toBe(true); + expect(hasTime("2018-01-01T00:00:00")).toBe(true); + expect(hasTime("2018-01-01T00:00:00Z")).toBe(true); + expect(hasTime("2018-01-01T00:00:00+01:00")).toBe(true); + + // We're currently not using this format, but just for good measure + expect(hasTime("2018-01-01T01")).toBe(true); + expect(hasTime("2018-01-01T0102")).toBe(true); + expect(hasTime("2018-01-01T010203")).toBe(true); + }); + + test("Check if ISO 8601 string has timezone information", () => { + expect(hasTimezone("2018-01-01")).toBe(false); + expect(hasTimezone("2018-01-01T00:00:00")).toBe(false); + + expect(hasTimezone("2018-01-01T00:00:00Z")).toBe(true); + expect(hasTimezone("2018-01-01T00:00:00+01:00")).toBe(true); + expect(hasTimezone("2018-01-01T00:00:00-02:00")).toBe(true); + expect(hasTimezone("2018-01-01T00:00:00+03")).toBe(true); + expect(hasTimezone("2018-01-01T00:00:00-04")).toBe(true); + + // We're currently not using this format, but just for good measure + expect(hasTime("2018-01-01T010203Z")).toBe(true); + expect(hasTime("2018-01-01T010203+01")).toBe(true); + expect(hasTime("2018-01-01T010203+01:00")).toBe(true); + }); + + test("Convert ISO string to timestamp", () => { + // Test data generated with: https://www.timestamp-converter.com/ + expect(isoStringToTimestampUtcMs("2018-01-01T00:00:00Z")).toBe(1514764800000); + expect(isoStringToTimestampUtcMs("2018-01-01T00:00:00")).toBe(1514764800000); + + expect(isoStringToTimestampUtcMs("2018-01-01")).toBe(1514764800000); + + expect(isoStringToTimestampUtcMs("2018-01-01T00:00:00.001")).toBe(1514764800001); + expect(isoStringToTimestampUtcMs("2018-01-01T00:00:00.001Z")).toBe(1514764800001); + expect(isoStringToTimestampUtcMs("2017-12-31T23:59:59.999")).toBe(1514764799999); + expect(isoStringToTimestampUtcMs("2017-12-31T23:59:59.999Z")).toBe(1514764799999); + }); + + test("Convert timestamp to ISO string", () => { + // Test data generated with: https://www.timestamp-converter.com/ + expect(timestampUtcMsToIsoString(1514764800000)).toBe("2018-01-01T00:00:00.000Z"); + expect(timestampUtcMsToIsoString(1514764800001)).toBe("2018-01-01T00:00:00.001Z"); + expect(timestampUtcMsToIsoString(1514764799999)).toBe("2017-12-31T23:59:59.999Z"); + }); + + test("Convert timestamp to compact ISO string", () => { + // Test data generated with: https://www.timestamp-converter.com/ + expect(timestampUtcMsToCompactIsoString(1514764799999)).toBe("2017-12-31T23:59:59.999Z"); + expect(timestampUtcMsToCompactIsoString(1514764800001)).toBe("2018-01-01T00:00:00.001Z"); + expect(timestampUtcMsToCompactIsoString(1514764801000)).toBe("2018-01-01T00:00:01Z"); + expect(timestampUtcMsToCompactIsoString(1514764800000)).toBe("2018-01-01"); + }); +});