diff --git a/webapp/eslint.config.js b/webapp/eslint.config.js index fc16942269..28b56ecf07 100644 --- a/webapp/eslint.config.js +++ b/webapp/eslint.config.js @@ -129,7 +129,7 @@ export default [ { // Includes hooks from 'react-use' additionalHooks: - "(useSafeMemo|useUpdateEffectOnce|useDeepCompareEffect|useShallowCompareEffect|useCustomCompareEffect)", + "(useSafeMemo|useUpdateEffect|useUpdateEffectOnce|useDeepCompareEffect|useShallowCompareEffect|useCustomCompareEffect)", }, ], "require-await": "warn", // TODO: switch to "error" when the quantity of warning will be low diff --git a/webapp/src/components/common/DataGrid.tsx b/webapp/src/components/common/DataGrid.tsx index 2f95b302d6..82cb0bff74 100644 --- a/webapp/src/components/common/DataGrid.tsx +++ b/webapp/src/components/common/DataGrid.tsx @@ -37,10 +37,10 @@ type RowMarkers = type RowMarkersOptions = Exclude; -export interface DataGridProps - extends Omit { +export interface DataGridProps extends Omit { rowMarkers?: RowMarkers; enableColumnResize?: boolean; + onGridSelectionChange?: (selection: GridSelection) => void; } function isStringRowMarkerOptions( @@ -61,6 +61,7 @@ function DataGrid(props: DataGridProps) { onColumnResizeEnd, enableColumnResize = true, freezeColumns, + onGridSelectionChange, ...rest } = props; @@ -69,12 +70,13 @@ function DataGrid(props: DataGridProps) { const isStringRowMarkers = isStringRowMarkerOptions(rowMarkersOptions); const adjustedFreezeColumns = isStringRowMarkers ? (freezeColumns || 0) + 1 : freezeColumns; - const [columns, setColumns] = useState(columnsFromProps); - const [selection, setSelection] = useState({ - columns: CompactSelection.empty(), + const [gridSelection, setGridSelection] = useState({ rows: CompactSelection.empty(), + columns: CompactSelection.empty(), }); + const [columns, setColumns] = useState(columnsFromProps); + // Add a column for the "string" row markers if needed useEffect(() => { setColumns( @@ -223,7 +225,7 @@ function DataGrid(props: DataGridProps) { if (newSelection.current) { // Select the whole row when clicking on a row marker cell if (rowMarkersOptions.kind === "clickable-string" && newSelection.current.cell[0] === 0) { - setSelection({ + onGridSelectionChange?.({ ...newSelection, current: undefined, rows: CompactSelection.fromSingleSelection(newSelection.current.cell[1]), @@ -241,16 +243,7 @@ function DataGrid(props: DataGridProps) { // Prevent selecting the row marker column if (newSelection.columns.hasIndex(0)) { // TODO find a way to have the rows length to select all the rows like other row markers - // setSelection({ - // ...newSelection, - // columns: CompactSelection.empty(), - // rows: CompactSelection.fromSingleSelection([ - // 0, - // // rowsLength - // ]), - // }); - - setSelection({ + onGridSelectionChange?.({ ...newSelection, columns: newSelection.columns.remove(0), }); @@ -259,7 +252,8 @@ function DataGrid(props: DataGridProps) { } } - setSelection(newSelection); + setGridSelection(newSelection); + onGridSelectionChange?.(newSelection); } }; @@ -287,7 +281,7 @@ function DataGrid(props: DataGridProps) { onColumnResize={handleColumnResize} onColumnResizeStart={handleColumnResizeStart} onColumnResizeEnd={handleColumnResizeEnd} - gridSelection={selection} + gridSelection={gridSelection} onGridSelectionChange={handleGridSelectionChange} freezeColumns={adjustedFreezeColumns} /> diff --git a/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx b/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx index 572ce68883..2831b4d507 100644 --- a/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx +++ b/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx @@ -13,21 +13,21 @@ */ import { + CompactSelection, GridCellKind, + type GridSelection, type EditableGridCell, type EditListItem, type Item, } from "@glideapps/glide-data-grid"; import { useGridCellContent } from "../../hooks/useGridCellContent"; -import { useMemo } from "react"; -import { - type EnhancedGridColumn, - type GridUpdate, - type MatrixAggregates, -} from "../../shared/types"; +import { useMemo, useState } from "react"; +import DataGrid from "@/components/common/DataGrid"; import { useColumnMapping } from "../../hooks/useColumnMapping"; +import type { EnhancedGridColumn, MatrixAggregates, GridUpdate } from "../../shared/types"; import { darkTheme, readOnlyDarkTheme } from "../../styles"; -import DataGrid from "@/components/common/DataGrid"; +import MatrixStats from "../MatrixStats"; +import { useSelectionStats } from "../../hooks/useSelectionStats"; export interface MatrixGridProps { data: number[][]; @@ -42,6 +42,7 @@ export interface MatrixGridProps { onMultipleCellsEdit?: (updates: GridUpdate[]) => void; readOnly?: boolean; showPercent?: boolean; + showStats?: boolean; } function MatrixGrid({ @@ -57,9 +58,21 @@ function MatrixGrid({ onMultipleCellsEdit, readOnly, showPercent, + showStats = true, }: MatrixGridProps) { + const [gridSelection, setGridSelection] = useState({ + rows: CompactSelection.empty(), + columns: CompactSelection.empty(), + }); + const { gridToData } = useColumnMapping(columns); + const selectionStats = useSelectionStats({ + data, + selection: gridSelection, + gridToData, + }); + const theme = useMemo(() => { if (readOnly) { return { @@ -140,23 +153,27 @@ function MatrixGrid({ //////////////////////////////////////////////////////////////// return ( - + <> + + {showStats && } + ); } diff --git a/webapp/src/components/common/Matrix/components/MatrixStats/index.tsx b/webapp/src/components/common/Matrix/components/MatrixStats/index.tsx new file mode 100644 index 0000000000..6770242a7b --- /dev/null +++ b/webapp/src/components/common/Matrix/components/MatrixStats/index.tsx @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2025, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +import { Box, Paper, Typography, Divider } from "@mui/material"; +import { formatGridNumber } from "../../shared/utils"; + +interface MatrixStatsProps { + stats: { + count: number; + sum: number; + average: number; + min: number; + max: number; + } | null; +} + +function MatrixStats({ stats }: MatrixStatsProps) { + if (!stats) { + return null; + } + + const statItems = [ + { label: "Nb", value: stats.count }, + { label: "Total", value: formatGridNumber({ value: stats.sum }) }, + { + label: "Avg", + value: formatGridNumber({ value: stats.average, maxDecimals: 2 }), + }, + { label: "Min", value: formatGridNumber({ value: stats.min }) }, + { label: "Max", value: formatGridNumber({ value: stats.max }) }, + ]; + + return ( + + {statItems.map((item, index) => ( + + + {item.label} + + + {item.value} + + {index < statItems.length - 1 && ( + + )} + + ))} + + ); +} + +export default MatrixStats; diff --git a/webapp/src/components/common/Matrix/hooks/useSelectionStats/index.ts b/webapp/src/components/common/Matrix/hooks/useSelectionStats/index.ts new file mode 100644 index 0000000000..d6c38ca79f --- /dev/null +++ b/webapp/src/components/common/Matrix/hooks/useSelectionStats/index.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2025, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +import type { GridSelection, Item } from "@glideapps/glide-data-grid"; +import { useState } from "react"; +import { useUpdateEffect } from "react-use"; + +interface SelectionStats { + sum: number; + average: number; + min: number; + max: number; + count: number; +} + +interface UseSelectionStatsProps { + data: number[][]; + selection: GridSelection; + gridToData: (coordinates: Item) => Item | null; +} + +export function useSelectionStats({ data, selection, gridToData }: UseSelectionStatsProps) { + const [stats, setStats] = useState(null); + + useUpdateEffect(() => { + let sum = 0; + let min = Infinity; + let max = -Infinity; + let count = 0; + const numRows = data.length; + const numCols = data[0]?.length ?? 0; + + const processValue = (value: number) => { + sum += value; + min = Math.min(min, value); + max = Math.max(max, value); + count++; + }; + + if (selection.current?.range) { + const { x, y, width, height } = selection.current.range; + + for (let col = x; col < x + width; ++col) { + for (let row = y; row < y + height; ++row) { + const coordinates = gridToData([col, row]); + + if (coordinates) { + const [dataCol, dataRow] = coordinates; + const value = data[dataRow]?.[dataCol]; + + if (value !== undefined) { + processValue(value); + } + } + } + } + } + + for (const row of selection.rows) { + for (let col = 0; col < numCols; ++col) { + const value = data[row]?.[col]; + + if (value !== undefined) { + processValue(value); + } + } + } + + for (const col of selection.columns) { + for (let row = 0; row < numRows; ++row) { + const coordinates = gridToData([col, row]); + + if (coordinates) { + const [dataCol, dataRow] = coordinates; + const value = data[dataRow]?.[dataCol]; + + if (value !== undefined) { + processValue(value); + } + } + } + } + + setStats(count ? { sum, min, max, count, average: sum / count } : null); + }, [data, selection]); + + return stats; +}