diff --git a/antarest/study/web/raw_studies_blueprint.py b/antarest/study/web/raw_studies_blueprint.py index dc8554e55d..88c45714b2 100644 --- a/antarest/study/web/raw_studies_blueprint.py +++ b/antarest/study/web/raw_studies_blueprint.py @@ -18,6 +18,7 @@ import typing as t from pathlib import Path, PurePosixPath +import numpy as np from fastapi import APIRouter, Body, Depends, File, HTTPException from fastapi.params import Param, Query from starlette.responses import FileResponse, JSONResponse, PlainTextResponse, Response, StreamingResponse @@ -114,6 +115,7 @@ def get_study( path: str = Param("/", examples=get_path_examples()), # type: ignore depth: int = 3, formatted: bool = True, + aggregates: t.Optional[bool] = Query(False, description="Whether to calculate aggregates for 2D matrices"), current_user: JWTUser = Depends(auth.get_current_user), ) -> t.Any: """ @@ -136,6 +138,17 @@ def get_study( parameters = RequestParameters(user=current_user) output = study_service.get(uuid, path, depth=depth, formatted=formatted, params=parameters) + # Temporary workaround to calculate aggregates for 2D matrices + if aggregates and isinstance(output, dict) and "data" in output: + matrix = np.asarray(output["data"]) + if matrix.ndim == 2: + aggregate_values = { + "min": np.nanmin(matrix, axis=1).tolist(), + "max": np.nanmax(matrix, axis=1).tolist(), + "avg": np.nanmean(matrix, axis=1).tolist(), + } + output["aggregates"] = aggregate_values + if isinstance(output, bytes): # Guess the suffix form the target data resource_path = PurePosixPath(path) @@ -189,6 +202,7 @@ def get_study( indent=None, separators=(",", ":"), ).encode("utf-8") + return Response(content=json_response, media_type="application/json") @bp.get( diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts index 5544255e52..b4edbb485d 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts @@ -1,9 +1,6 @@ -import { - StudyMetadata, - Area, - MatrixType, -} from "../../../../../../../../common/types"; +import { StudyMetadata, Area } from "../../../../../../../../common/types"; import client from "../../../../../../../../services/api/client"; +import { MatrixDataDTO } from "../../../../../../../common/MatrixGrid/types"; import { AreaCoefficientItem } from "../utils"; //////////////////////////////////////////////////////////////// @@ -44,7 +41,7 @@ export async function setAllocationFormFields( export const getAllocationMatrix = async ( studyId: StudyMetadata["id"], -): Promise => { +): Promise => { const res = await client.get( `v1/studies/${studyId}/areas/hydro/allocation/matrix`, ); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts index bd5e574389..03292acc03 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts @@ -1,9 +1,6 @@ -import { - StudyMetadata, - Area, - MatrixType, -} from "../../../../../../../../common/types"; +import { StudyMetadata, Area } from "../../../../../../../../common/types"; import client from "../../../../../../../../services/api/client"; +import { MatrixDataDTO } from "../../../../../../../common/MatrixGrid/types"; import { AreaCoefficientItem } from "../utils"; //////////////////////////////////////////////////////////////// @@ -44,7 +41,7 @@ export async function setCorrelationFormFields( export async function getCorrelationMatrix( studyId: StudyMetadata["id"], -): Promise { +): Promise { const res = await client.get( `v1/studies/${studyId}/areas/hydro/correlation/matrix`, ); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx index acb3219c25..e38a2f6ee2 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx @@ -2,6 +2,7 @@ import useAppSelector from "../../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../../redux/selectors"; import { MATRICES, HydroMatrixType } from "./utils"; import Matrix from "../../../../../../common/MatrixGrid/Matrix"; +import { Box } from "@mui/material"; interface Props { type: HydroMatrixType; @@ -17,14 +18,18 @@ function HydroMatrix({ type }: Props) { //////////////////////////////////////////////////////////////// return ( - + + + ); } diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx index cca7f9519a..3b211aed31 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx @@ -36,7 +36,9 @@ function Hydro() { // JSX //////////////////////////////////////////////////////////////// - return ; + return ( + + ); } export default Hydro; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index cb1aa1c50c..9fb419918a 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -1,4 +1,4 @@ -import { MatrixType } from "../../../../../../../common/types"; +import { MatrixDataDTO } from "../../../../../../common/MatrixGrid/types"; import { SplitViewProps } from "../../../../../../common/SplitView"; import { getAllocationMatrix } from "./Allocation/utils"; import { getCorrelationMatrix } from "./Correlation/utils"; @@ -26,7 +26,7 @@ export const HydroMatrix = { // Types //////////////////////////////////////////////////////////////// -export type fetchMatrixFn = (studyId: string) => Promise; +export type fetchMatrixFn = (studyId: string) => Promise; export type HydroMatrixType = (typeof HydroMatrix)[keyof typeof HydroMatrix]; export interface HydroMatrixProps { @@ -35,6 +35,7 @@ export interface HydroMatrixProps { columns?: string[]; rowHeaders?: string[]; fetchFn?: fetchMatrixFn; + enableDateTimeColumn?: boolean; enableReadOnly?: boolean; enablePercentDisplay?: boolean; } @@ -112,6 +113,7 @@ export const MATRICES: Matrices = { url: "input/hydro/common/capacity/creditmodulations_{areaId}", columns: generateColumns("%"), rowHeaders: ["Generating Power", "Pumping Power"], + enableDateTimeColumn: false, enablePercentDisplay: true, }, [HydroMatrix.EnergyCredits]: { @@ -133,7 +135,7 @@ export const MATRICES: Matrices = { [HydroMatrix.WaterValues]: { title: "Water Values", url: "input/hydro/common/capacity/waterValues_{areaId}", - // columns: generateColumns("%"), // TODO this causes the data is undefined error + // columns: generateColumns("%"), // TODO this causes Runtime error to be fixed }, [HydroMatrix.HydroStorage]: { title: "Hydro Storage", @@ -181,6 +183,7 @@ export const MATRICES: Matrices = { title: "Allocation", url: "", fetchFn: getAllocationMatrix, + enableDateTimeColumn: false, enableReadOnly: true, enablePercentDisplay: true, }, @@ -188,6 +191,7 @@ export const MATRICES: Matrices = { title: "Correlation", url: "", fetchFn: getCorrelationMatrix, + enableDateTimeColumn: false, enableReadOnly: true, enablePercentDisplay: true, }, diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx index 0271e3b5a8..0101725390 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx @@ -10,7 +10,7 @@ function Load() { // JSX //////////////////////////////////////////////////////////////// - return ; + return ; } export default Load; diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index 03cf64dd22..e3c2b1fcd4 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -9,11 +9,13 @@ import { StudyMetadata } from "../../../common/types"; import { MatrixContainer, MatrixHeader, MatrixTitle } from "./style"; import MatrixActions from "./MatrixActions"; import EmptyView from "../page/SimpleContent"; +import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; interface MatrixProps { url: string; title?: string; - rowHeaders?: string[]; + customRowHeaders?: string[]; + enableDateTimeColumn?: boolean; enableTimeSeriesColumns?: boolean; enableAggregateColumns?: boolean; enableRowHeaders?: boolean; @@ -21,19 +23,22 @@ interface MatrixProps { enableReadOnly?: boolean; customColumns?: string[] | readonly string[]; colWidth?: number; + fetchMatrixData?: fetchMatrixFn; } function Matrix({ url, title = "global.timeSeries", - rowHeaders = [], + customRowHeaders = [], + enableDateTimeColumn = true, enableTimeSeriesColumns = true, enableAggregateColumns = false, - enableRowHeaders = rowHeaders.length > 0, + enableRowHeaders = customRowHeaders.length > 0, enablePercentDisplay = false, enableReadOnly = false, customColumns, colWidth, + fetchMatrixData, }: MatrixProps) { const { t } = useTranslation(); const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -41,6 +46,7 @@ function Matrix({ const { data, + aggregates, error, isLoading, isSubmitting, @@ -58,11 +64,13 @@ function Matrix({ } = useMatrix( study.id, url, + enableDateTimeColumn, enableTimeSeriesColumns, enableAggregateColumns, enableRowHeaders, customColumns, colWidth, + fetchMatrixData, ); //////////////////////////////////////////////////////////////// @@ -102,9 +110,10 @@ function Matrix({ ; + aggregates?: MatrixAggregates; rowHeaders?: string[]; width?: string; height?: string; diff --git a/webapp/src/components/common/MatrixGrid/types.ts b/webapp/src/components/common/MatrixGrid/types.ts index 8f13d3666c..bed5b3f6c3 100644 --- a/webapp/src/components/common/MatrixGrid/types.ts +++ b/webapp/src/components/common/MatrixGrid/types.ts @@ -52,11 +52,19 @@ export interface EnhancedGridColumn extends BaseGridColumn { type: ColumnType; editable: boolean; } + +export interface MatrixAggregates { + min: number[]; + max: number[]; + avg: number[]; +} + // Represents data coming from the API export interface MatrixDataDTO { data: number[][]; columns: number[]; index: number[]; + aggregates?: MatrixAggregates; } export type Coordinates = [number, number]; diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts index 5ee85e5882..8ae2a7f8b3 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts @@ -1,6 +1,11 @@ import { useCallback, useMemo } from "react"; import { GridCell, GridCellKind, Item } from "@glideapps/glide-data-grid"; -import { type EnhancedGridColumn, type ColumnType, ColumnTypes } from "./types"; +import { + type EnhancedGridColumn, + type ColumnType, + ColumnTypes, + MatrixAggregates, +} from "./types"; import { formatDateTime } from "./utils"; type CellContentGenerator = ( @@ -9,7 +14,7 @@ type CellContentGenerator = ( column: EnhancedGridColumn, data: number[][], dateTime?: string[], - aggregates?: Record, + aggregates?: MatrixAggregates, rowHeaders?: string[], ) => GridCell; @@ -52,14 +57,14 @@ const cellContentGenerators: Record = { }; }, [ColumnTypes.Aggregate]: (row, col, column, data, dateTime, aggregates) => { - const value = aggregates?.[column.id]?.[row]; + const value = aggregates?.[column.id as keyof MatrixAggregates]?.[row]; return { kind: GridCellKind.Number, data: value, displayData: value?.toString() ?? "", readonly: !column.editable, - allowOverlay: false, + allowOverlay: true, }; }, }; @@ -95,7 +100,7 @@ export function useGridCellContent( columns: EnhancedGridColumn[], gridToData: (cell: Item) => Item | null, dateTime?: string[], - aggregates?: Record, + aggregates?: MatrixAggregates, rowHeaders?: string[], isReadOnlyEnabled = false, isPercentDisplayEnabled = false, @@ -156,19 +161,21 @@ export function useGridCellContent( rowHeaders, ); - // Prevent updates for read-only grids - if (isReadOnlyEnabled) { + // Display number values as percentages if enabled + if (isPercentDisplayEnabled && gridCell.kind === GridCellKind.Number) { return { ...gridCell, - allowOverlay: false, + displayData: `${gridCell.data}%`, + // If ReadOnly is enabled, we don't want to allow overlay + allowOverlay: !isReadOnlyEnabled, }; } - // Display number values as percentages if enabled - if (isPercentDisplayEnabled && gridCell.kind === GridCellKind.Number) { + // Prevent updates for read-only grids + if (isReadOnlyEnabled) { return { ...gridCell, - displayData: `${gridCell.data}%`, + allowOverlay: false, }; } diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index 13a43a25b0..86ddaff6d8 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -16,23 +16,31 @@ import { GridUpdate, MatrixUpdateDTO, } from "./types"; -import { generateDataColumns, generateDateTime } from "./utils"; +import { + aggregatesTheme, + generateDataColumns, + generateDateTime, +} from "./utils"; import useUndo from "use-undo"; import { GridCellKind } from "@glideapps/glide-data-grid"; +import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; interface DataState { - data: number[][]; + data: MatrixDataDTO["data"]; + aggregates: MatrixDataDTO["aggregates"]; pendingUpdates: MatrixUpdateDTO[]; } export function useMatrix( studyId: string, url: string, + enableDateTimeColumn: boolean, enableTimeSeriesColumns: boolean, enableAggregateColumns: boolean, enableRowHeaders?: boolean, customColumns?: string[] | readonly string[], colWidth?: number, + fetchMatrixData?: fetchMatrixFn, ) { const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); const [columnCount, setColumnCount] = useState(0); @@ -41,27 +49,63 @@ export function useMatrix( const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(undefined); const [{ present: currentState }, { set: setState, undo, redo, canRedo }] = - useUndo({ data: [], pendingUpdates: [] }); + useUndo({ + data: [], + aggregates: { min: [], max: [], avg: [] }, + pendingUpdates: [], + }); - const fetchMatrix = useCallback(async () => { - setIsLoading(true); - try { - const [matrix, index] = await Promise.all([ - getStudyData(studyId, url), - getStudyMatrixIndex(studyId, url), - ]); - - setState({ data: matrix.data, pendingUpdates: [] }); - setColumnCount(matrix.columns.length); - setIndex(index); - setIsLoading(false); - } catch (error) { - setError(new Error(t("data.error.matrix"))); - enqueueErrorSnackbar(t("data.error.matrix"), error as AxiosError); - } finally { - setIsLoading(false); - } - }, [enqueueErrorSnackbar, setState, studyId, url]); + const fetchMatrix = useCallback( + async (loadingState = true) => { + // !NOTE This is a temporary solution to ensure the matrix is up to date + // TODO: Remove this once the matrix API is updated to return the correct data + if (loadingState) { + setIsLoading(true); + } + + try { + const [matrix, index] = await Promise.all([ + fetchMatrixData + ? // If a custom fetch function is provided, use it + fetchMatrixData(studyId) + : getStudyData( + studyId, + url, + 1, + enableAggregateColumns, + ), + getStudyMatrixIndex(studyId, url), + ]); + + setState({ + data: matrix.data, + aggregates: matrix.aggregates, + pendingUpdates: [], + }); + setColumnCount(matrix.columns.length); + setIndex(index); + setIsLoading(false); + + return { + matrix, + index, + }; + } catch (error) { + setError(new Error(t("data.error.matrix"))); + enqueueErrorSnackbar(t("data.error.matrix"), error as AxiosError); + } finally { + setIsLoading(false); + } + }, + [ + enableAggregateColumns, + enqueueErrorSnackbar, + fetchMatrixData, + setState, + studyId, + url, + ], + ); useEffect(() => { fetchMatrix(); @@ -76,14 +120,16 @@ export function useMatrix( return []; } - const baseColumns: EnhancedGridColumn[] = [ - { + const baseColumns: EnhancedGridColumn[] = []; + + if (enableDateTimeColumn) { + baseColumns.push({ id: "date", title: "Date", type: ColumnTypes.DateTime, editable: false, - }, - ]; + }); + } if (enableRowHeaders) { baseColumns.unshift({ @@ -107,22 +153,22 @@ export function useMatrix( id: "min", title: "Min", type: ColumnTypes.Aggregate, - width: 50, editable: false, + themeOverride: aggregatesTheme, }, { id: "max", title: "Max", type: ColumnTypes.Aggregate, - width: 50, editable: false, + themeOverride: aggregatesTheme, }, { id: "avg", title: "Avg", type: ColumnTypes.Aggregate, - width: 50, editable: false, + themeOverride: aggregatesTheme, }, ] : []; @@ -130,6 +176,7 @@ export function useMatrix( return [...baseColumns, ...dataColumns, ...aggregateColumns]; }, [ currentState.data, + enableDateTimeColumn, enableRowHeaders, enableTimeSeriesColumns, columnCount, @@ -165,6 +212,7 @@ export function useMatrix( setState({ data: updatedData, + aggregates: currentState.aggregates, pendingUpdates: [...currentState.pendingUpdates, ...newUpdates], }); }, @@ -194,12 +242,23 @@ export function useMatrix( } setIsSubmitting(true); + try { await updateMatrix(studyId, url, currentState.pendingUpdates); - setState({ data: currentState.data, pendingUpdates: [] }); + + setState({ + data: currentState.data, + aggregates: currentState.aggregates, + pendingUpdates: [], + }); + enqueueSnackbar(t("matrix.success.matrixUpdate"), { variant: "success", }); + + // !NOTE This is a temporary solution to ensure the matrix is up to date + // TODO: Remove this once the matrix API is updated to return the correct data + await fetchMatrix(false); } catch (error) { setError(new Error(t("matrix.error.matrixUpdate"))); enqueueErrorSnackbar(t("matrix.error.matrixUpdate"), error as AxiosError); @@ -223,6 +282,7 @@ export function useMatrix( return { data: currentState.data, + aggregates: currentState.aggregates, error, isLoading, isSubmitting, diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index 23c41b354e..c9e7d8834c 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -57,6 +57,23 @@ export const readOnlyDarkTheme: Partial = { drilldownBorder: "rgba(255, 255, 255, 0.2)", }; +export const aggregatesTheme: Partial = { + bgCell: "#31324A", + bgCellMedium: "#383A5C", + textDark: "#1976D2", + textMedium: "#2196F3", + textLight: "#64B5F6", + accentColor: "#2196F3", + accentLight: "#64B5F633", + fontFamily: "Inter, sans-serif", + baseFontStyle: "bold 13px", + editorFontSize: "13px", + headerFontStyle: "bold 11px", + accentFg: "#2196F3", // This affects the selection border + borderColor: "#2196F3", // This affects the general cell borders + drilldownBorder: "#2196F3", // This affects the border when drilling down into a cell +}; + const dateIncrementStrategies: Record< MatrixIndex["level"], DateIncrementStrategy @@ -180,7 +197,7 @@ export function generateTimeSeriesColumns({ count, startIndex = 1, prefix = "TS", - width = 50, + width, editable = true, style = "normal", }: TimeSeriesColumnOptions): EnhancedGridColumn[] { diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index a060bd8a16..7b60938a1a 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -44,9 +44,12 @@ export const getStudyData = async ( sid: string, path = "", depth = 1, + aggregates = false, ): Promise => { const res = await client.get( - `/v1/studies/${sid}/raw?path=${encodeURIComponent(path)}&depth=${depth}`, + `/v1/studies/${sid}/raw?path=${encodeURIComponent( + path, + )}&depth=${depth}&aggregates=${aggregates}`, ); return res.data; };