Skip to content

Commit

Permalink
feat(ui-matrix): add matrix selection stats
Browse files Browse the repository at this point in the history
  • Loading branch information
hdinia committed Jan 24, 2025
1 parent 5c3f165 commit 6dcf8d5
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 43 deletions.
2 changes: 1 addition & 1 deletion webapp/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 12 additions & 18 deletions webapp/src/components/common/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ type RowMarkers =

type RowMarkersOptions = Exclude<RowMarkers, string>;

export interface DataGridProps
extends Omit<DataEditorProps, "rowMarkers" | "onGridSelectionChange" | "gridSelection"> {
export interface DataGridProps extends Omit<DataEditorProps, "rowMarkers" | "gridSelection"> {
rowMarkers?: RowMarkers;
enableColumnResize?: boolean;
onGridSelectionChange?: (selection: GridSelection) => void;
}

function isStringRowMarkerOptions(
Expand All @@ -61,6 +61,7 @@ function DataGrid(props: DataGridProps) {
onColumnResizeEnd,
enableColumnResize = true,
freezeColumns,
onGridSelectionChange,
...rest
} = props;

Expand All @@ -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<GridSelection>({
columns: CompactSelection.empty(),
const [gridSelection, setGridSelection] = useState<GridSelection>({
rows: CompactSelection.empty(),
columns: CompactSelection.empty(),
});

const [columns, setColumns] = useState(columnsFromProps);

// Add a column for the "string" row markers if needed
useEffect(() => {
setColumns(
Expand Down Expand Up @@ -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]),
Expand All @@ -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),
});
Expand All @@ -259,7 +252,8 @@ function DataGrid(props: DataGridProps) {
}
}

setSelection(newSelection);
setGridSelection(newSelection);
onGridSelectionChange?.(newSelection);
}
};

Expand Down Expand Up @@ -287,7 +281,7 @@ function DataGrid(props: DataGridProps) {
onColumnResize={handleColumnResize}
onColumnResizeStart={handleColumnResizeStart}
onColumnResizeEnd={handleColumnResizeEnd}
gridSelection={selection}
gridSelection={gridSelection}
onGridSelectionChange={handleGridSelectionChange}
freezeColumns={adjustedFreezeColumns}
/>
Expand Down
65 changes: 41 additions & 24 deletions webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[][];
Expand All @@ -42,6 +42,7 @@ export interface MatrixGridProps {
onMultipleCellsEdit?: (updates: GridUpdate[]) => void;
readOnly?: boolean;
showPercent?: boolean;
showStats?: boolean;
}

function MatrixGrid({
Expand All @@ -57,9 +58,21 @@ function MatrixGrid({
onMultipleCellsEdit,
readOnly,
showPercent,
showStats = true,
}: MatrixGridProps) {
const [gridSelection, setGridSelection] = useState<GridSelection>({
rows: CompactSelection.empty(),
columns: CompactSelection.empty(),
});

const { gridToData } = useColumnMapping(columns);

const selectionStats = useSelectionStats({
data,
selection: gridSelection,
gridToData,
});

const theme = useMemo(() => {
if (readOnly) {
return {
Expand Down Expand Up @@ -140,23 +153,27 @@ function MatrixGrid({
////////////////////////////////////////////////////////////////

return (
<DataGrid
theme={theme}
width={width}
height={height}
rows={rows}
columns={columns}
getCellContent={getCellContent}
onCellEdited={handleCellEdited}
onCellsEdited={handleCellsEdited}
keybindings={{ paste: false, copy: false }}
getCellsForSelection // TODO handle large copy/paste using this
fillHandle
allowedFillDirections="any"
rowMarkers="both"
freezeColumns={1} // Make the first column sticky
cellActivationBehavior="second-click"
/>
<>
<DataGrid
theme={theme}
width={width}
height={height}
rows={rows}
columns={columns}
getCellContent={getCellContent}
onCellEdited={handleCellEdited}
onCellsEdited={handleCellsEdited}
keybindings={{ paste: false, copy: false }}
getCellsForSelection // TODO handle large copy/paste using this
fillHandle
allowedFillDirections="any"
rowMarkers="both"
freezeColumns={1} // Make the first column sticky
cellActivationBehavior="second-click"
onGridSelectionChange={setGridSelection}
/>
{showStats && <MatrixStats stats={selectionStats} />}
</>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Paper
sx={{
display: "flex",
flex: 1,
alignItems: "center",
justifyContent: "flex-end",
width: 1,
p: 1,
maxHeight: 30,
}}
>
{statItems.map((item, index) => (
<Box
key={item.label}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Typography
sx={{
display: "flex",
p: 0.5,
alignItems: "center",
color: "lightgray",
fontSize: 10,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.1em",
}}
>
{item.label}
</Typography>
<Typography
sx={{
display: "flex",
alignItems: "center",
fontSize: 15,
}}
>
{item.value}
</Typography>
{index < statItems.length - 1 && (
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
)}
</Box>
))}
</Paper>
);
}

export default MatrixStats;
Original file line number Diff line number Diff line change
@@ -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<SelectionStats | null>(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;
}

0 comments on commit 6dcf8d5

Please sign in to comment.