Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade realization filtering #741

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0c223cb
Prototyping backup - WIP
jorgenherje Sep 27, 2024
6339295
WIP
jorgenherje Sep 30, 2024
038af15
Parameter value filtering
jorgenherje Oct 2, 2024
0b718fc
Make parameter selections map and selections readonly
jorgenherje Oct 3, 2024
c90e0e6
Adjust gap for radio group
jorgenherje Oct 4, 2024
42b0470
Add realization number selector component and upgrade functionality
jorgenherje Oct 4, 2024
16b0167
Improve categorization of discrete/numerical parameters
jorgenherje Oct 7, 2024
68ffc27
Replace AddItemButton with SmartNodeSelector + minor fixes
jorgenherje Oct 8, 2024
7376d47
Fix format error
jorgenherje Oct 9, 2024
0cd7a72
Merge branch 'main' into improve-realization-filter
jorgenherje Oct 9, 2024
578a945
Merge branch 'main' into improve-realization-filter
jorgenherje Oct 9, 2024
0b0649a
BACKUP
jorgenherje Oct 9, 2024
12012f1
Add compact visualization for selected realization number display
jorgenherje Oct 10, 2024
dda1519
Adjust opacity based on active ensemble filters
jorgenherje Oct 11, 2024
4fe595c
New suggestions for visualizations in realization number display
jorgenherje Oct 11, 2024
92b2944
Use custom name or display name as Filter title
jorgenherje Oct 15, 2024
41c3527
Final compact/non-compact realization visualization
jorgenherje Oct 15, 2024
627634b
Fix issues with dynamic px values for tailwind class
jorgenherje Oct 17, 2024
3244c8d
Refactoring backup - WIP
jorgenherje Oct 18, 2024
875971a
Fix issues with realization picker tags and smart node selector tags
jorgenherje Oct 21, 2024
aff578d
Replace conditional render with css style "hidden" when filter is active
jorgenherje Oct 21, 2024
0133502
wip
rubenthoms Oct 22, 2024
72e0b64
Adjustments to `SmartNodeSelector`
rubenthoms Oct 22, 2024
9718380
Backup - WIP
jorgenherje Oct 22, 2024
7fa18d7
Merge remote-tracking branch 'rubenthoms/improve-realization-filter-s…
jorgenherje Oct 22, 2024
30e7d16
BACKUP
jorgenherje Oct 22, 2024
b2771d8
Adjust usage of SmartNodeSelector and minor fixes
jorgenherje Oct 23, 2024
7988250
Use React.useCallback in byRealizationNumberFilter
jorgenherje Oct 23, 2024
fa40c32
Merge branch 'main' into improve-realization-filter
jorgenherje Oct 23, 2024
a16be0b
Move realizationPicker utils into separate utils file
jorgenherje Oct 23, 2024
7d49285
Improve info/error text for invalid parameter selections
jorgenherje Oct 24, 2024
7579ce1
Improve algorithm for creating suggested realization number selections
jorgenherje Oct 24, 2024
cc22494
Fix unit test
jorgenherje Oct 24, 2024
8f5e8c1
Update realization filter tests
jorgenherje Oct 24, 2024
1860a30
Minor adjustments
jorgenherje Oct 25, 2024
23bb91e
Move EnsembleRealizationFilter to internal components
jorgenherje Oct 25, 2024
7f8ff46
Adjust path in unit test after moving of component
jorgenherje Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def parameter_table_to_ensemble_parameters(parameter_table: pa.Table) -> List[En
name=parameter_name,
group_name=f"LOG10_{group_name}" if is_logarithmic else group_name,
is_logarithmic=is_logarithmic,
is_numerical=parameter_table.schema.field(table_column_name).type != pa.string,
is_numerical=not _is_discrete_column(parameter_table.schema.field(table_column_name).type),
is_constant=len(set(parameter_table[table_column_name])) == 1,
descriptive_name=parameter_name,
values=parameter_table[table_column_name].to_numpy().tolist(),
Expand All @@ -150,6 +150,20 @@ def parameter_table_to_ensemble_parameters(parameter_table: pa.Table) -> List[En
return ensemble_parameters


def _is_discrete_column(column_type: pa.DataType) -> bool:
"""Check if a column is discrete

Discrete parameter is defined as a parameter that is either a string or an integer
"""
return (
column_type == pa.string()
or column_type == pa.int64()
or column_type == pa.int32()
or column_type == pa.int16()
or column_type == pa.int8()
)


def _parameter_name_and_group_name_to_parameter_str(parameter_name: str, group_name: Optional[str]) -> str:
"""Convert a parameter name and group name to a parameter string"""
return f"{group_name}:{parameter_name}" if group_name else parameter_name
Expand Down
335 changes: 282 additions & 53 deletions frontend/src/framework/RealizationFilter.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const ParameterListFilter: React.FC<ParameterListFilterProps> = (props: P
);

function handleSmartNodeSelectorChange(selection: SmartNodeSelectorSelection) {
setSelectedTags(selection.selectedTags);
setSelectedTags(selection.selectedTags.map((tag) => tag.text));
setSelectedNodes(selection.selectedNodes);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const Drawer: React.FC<DrawerProps> = (props) => {
</div>
)}
</div>
<div className="flex-grow flex flex-col">
<div className="flex-grow flex flex-col h-auto">
{props.showFilter && (
<div className="p-2 bg-slate-50">
<Input
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
import React from "react";

import { EnsembleParameters } from "@framework/EnsembleParameters";
import { RealizationFilter } from "@framework/RealizationFilter";
import {
IncludeExcludeFilter,
ParameterValueSelection,
RealizationFilterType,
RealizationFilterTypeStringMapping,
RealizationNumberSelection,
} from "@framework/types/realizationFilterTypes";
import { Button } from "@lib/components/Button";
import { Label } from "@lib/components/Label";
import { RadioGroup } from "@lib/components/RadioGroup";
import { resolveClassNames } from "@lib/utils/resolveClassNames";
import { Check, Clear } from "@mui/icons-material";

import { ByParameterValueFilter } from "./private-components/byParameterValueFilter";
import {
ByRealizationNumberFilter,
ByRealizationNumberFilterSelection,
} from "./private-components/byRealizationNumberFilter";
import { RealizationNumberDisplay } from "./private-components/realizationNumberDisplay";
import { createBestSuggestedRealizationNumberSelections } from "./private-utils/conversionUtils";

export type EnsembleRealizationFilterSelections = {
displayRealizationNumbers: readonly number[]; // For RealizationNumberDisplay
realizationNumberSelections: readonly RealizationNumberSelection[] | null; // For ByRealizationNumberFilter
parameterIdentStringToValueSelectionReadonlyMap: ReadonlyMap<string, ParameterValueSelection> | null; // For ByParameterValueFilter
filterType: RealizationFilterType;
includeOrExcludeFilter: IncludeExcludeFilter;
};

export type EnsembleRealizationFilterProps = {
selections: EnsembleRealizationFilterSelections;
hasUnsavedSelections: boolean;
ensembleName: string;
availableEnsembleRealizations: readonly number[];
ensembleParameters: EnsembleParameters;
isActive: boolean;
isAnotherFilterActive: boolean;
onClick?: () => void;
onHeaderClick?: () => void;
onFilterChange?: (newSelections: EnsembleRealizationFilterSelections) => void;
onApplyClick?: () => void;
onDiscardClick?: () => void;
};

/**
* Component for visualizing and handling of realization filtering for an Ensemble.
*
* Realization filter is used to filter ensemble realizations based on selected realization number or parameter values.
* The selection creates a valid subset of realization numbers for the ensemble throughout the application.
*/
export const EnsembleRealizationFilter: React.FC<EnsembleRealizationFilterProps> = (props) => {
const { onClick, onHeaderClick, onFilterChange, onApplyClick, onDiscardClick } = props;

// States for handling initial realization number selections and smart node selector tags
// - When undefined, the initial value will be calculated on next render
const [initialRealizationNumberSelections, setInitialRealizationNumberSelections] = React.useState<
readonly RealizationNumberSelection[] | null | undefined
>(props.selections.realizationNumberSelections);

// Update initial realization number selection due to conditional rendering
let actualInitialRealizationNumberSelections = initialRealizationNumberSelections;

// Reset the initial number selections to the current realization number selections when set to undefined
if (actualInitialRealizationNumberSelections === undefined) {
setInitialRealizationNumberSelections(props.selections.realizationNumberSelections);
actualInitialRealizationNumberSelections = props.selections.realizationNumberSelections;
}

function handleRealizationNumberFilterChanged(selection: ByRealizationNumberFilterSelection) {
if (!onFilterChange) {
return;
}

// Create realization number array to display based on current selection
const realizationNumberArray = RealizationFilter.createFilteredRealizationsFromRealizationNumberSelection(
selection.realizationNumberSelections,
props.availableEnsembleRealizations,
selection.includeOrExcludeFilter
);

onFilterChange({
...props.selections,
displayRealizationNumbers: realizationNumberArray,
realizationNumberSelections: selection.realizationNumberSelections,
includeOrExcludeFilter: selection.includeOrExcludeFilter,
});
}

function handleParameterValueFilterChanged(
newParameterIdentStringToValueSelectionMap: ReadonlyMap<string, ParameterValueSelection> | null
) {
if (!onFilterChange) {
return;
}

// Create realization number array to display based on current selection
const realizationNumberArray = RealizationFilter.createFilteredRealizationsFromParameterValueSelections(
newParameterIdentStringToValueSelectionMap,
props.ensembleParameters,
props.availableEnsembleRealizations
);

onFilterChange({
...props.selections,
displayRealizationNumbers: realizationNumberArray,
parameterIdentStringToValueSelectionReadonlyMap: newParameterIdentStringToValueSelectionMap,
});
}

function handleActiveFilterTypeChange(newFilterType: RealizationFilterType) {
if (!onFilterChange) {
return;
}

// Create realization number array to display based on current selection
let realizationNumberArray: readonly number[] = [];
if (newFilterType === RealizationFilterType.BY_REALIZATION_NUMBER) {
// Reset initial value to be calculated next render to ensure correct visualization when
// mounting realization number filter component
setInitialRealizationNumberSelections(undefined);

// Update realization numbers based on current selection
realizationNumberArray = RealizationFilter.createFilteredRealizationsFromRealizationNumberSelection(
props.selections.realizationNumberSelections,
props.availableEnsembleRealizations,
props.selections.includeOrExcludeFilter
);
} else if (newFilterType === RealizationFilterType.BY_PARAMETER_VALUES) {
// Create realization number array to display based on current parameters
realizationNumberArray = RealizationFilter.createFilteredRealizationsFromParameterValueSelections(
props.selections.parameterIdentStringToValueSelectionReadonlyMap,
props.ensembleParameters,
props.availableEnsembleRealizations
);
}

onFilterChange({
...props.selections,
filterType: newFilterType,
displayRealizationNumbers: realizationNumberArray,
});
}

function handleRealizationNumberDisplayClick(displayRealizationNumbers: readonly number[]) {
if (!onFilterChange) {
return;
}

// Create number selection based on the current display realization numbers
let candidateSelectedRealizationNumbers = displayRealizationNumbers;
if (props.selections.includeOrExcludeFilter === IncludeExcludeFilter.EXCLUDE_FILTER) {
// Invert selection for exclude filter
candidateSelectedRealizationNumbers = props.availableEnsembleRealizations.filter(
(realization) => !displayRealizationNumbers.includes(realization)
);
}

// Create realization number selections based on the current selection and available realization numbers
const newRealizationNumberSelections = createBestSuggestedRealizationNumberSelections(
candidateSelectedRealizationNumbers,
props.availableEnsembleRealizations
);

onFilterChange({
...props.selections,
displayRealizationNumbers: displayRealizationNumbers,
realizationNumberSelections: newRealizationNumberSelections,
});
}

function handleApplyClick() {
// Reset states for initialization on next render
setInitialRealizationNumberSelections(undefined);

if (onApplyClick) {
onApplyClick();
}
}

function handleDiscardClick() {
// Reset states for initialization on next render
setInitialRealizationNumberSelections(undefined);

if (onDiscardClick) {
onDiscardClick();
}
}

function handleOnClick() {
if (onClick && !props.isActive) {
onClick();
}
}

function handleHeaderOnClick() {
if (onHeaderClick && props.isActive) {
onHeaderClick();
}
}

return (
<div
className={resolveClassNames("outline mb-4 rounded-md", {
"cursor-pointer": !props.isActive,
"hover:opacity-75 transition-opacity duration-100": !props.isActive && props.isAnotherFilterActive,
"hover:outline-blue-400 hover:shadow-blue-400 hover:shadow-md":
!props.isActive && !props.isAnotherFilterActive,
"outline-orange-400 shadow-orange-400 shadow-lg": props.isActive && props.hasUnsavedSelections,
"outline-blue-400 shadow-blue-400 shadow-lg": props.isActive && !props.hasUnsavedSelections,
"opacity-100": props.isActive || !props.isAnotherFilterActive,
"opacity-60 ": !props.isActive && props.isAnotherFilterActive && props.hasUnsavedSelections,
"opacity-30": !props.isActive && props.isAnotherFilterActive && !props.hasUnsavedSelections,
"outline-2 outline-orange-400 shadow-orange-400 shadow-lg":
!props.isActive && props.hasUnsavedSelections,
"outline-2 outline-gray-300 shadow-gray-300 shadow-md": !props.isActive && !props.hasUnsavedSelections,
})}
onClick={handleOnClick}
>
<div
className={resolveClassNames({
"pointer-events-none": !props.isActive,
})}
>
<div className={`flex justify-center items-center p-2 rounded-md bg-slate-100 h-12 cursor-pointer`}>
<div
className="font-bold flex-grow text-sm overflow-ellipsis overflow-hidden whitespace-nowrap"
title={`Ensemble: ${props.ensembleName}`}
onClick={handleHeaderOnClick}
>
{props.ensembleName}
</div>
<div
className={resolveClassNames("flex items-center gap-1", {
hidden: !props.hasUnsavedSelections,
})}
>
<Button
variant="contained"
disabled={!props.hasUnsavedSelections}
size="small"
startIcon={<Check fontSize="small" />}
onClick={handleApplyClick}
/>
<Button
color="danger"
variant="contained"
disabled={!props.hasUnsavedSelections}
size="small"
startIcon={<Clear fontSize="small" />}
onClick={handleDiscardClick}
/>
</div>
</div>
<div className="flex flex-col gap-2 p-2">
<div className="border border-lightgrey p-2 rounded-md">
<RealizationNumberDisplay
selectedRealizations={props.selections.displayRealizationNumbers}
availableRealizations={props.availableEnsembleRealizations}
showAsCompact={!props.isActive}
disableInteraction={
props.selections.filterType !== RealizationFilterType.BY_REALIZATION_NUMBER ||
!props.isActive
}
onRealizationNumberClick={handleRealizationNumberDisplayClick}
/>
</div>
<div className={resolveClassNames({ hidden: !props.isActive })}>
<div className="border border-lightgrey rounded-md shadow-md p-2">
<Label text="Active Filter Type" wrapperClassName="border-b pb-2 mb-2">
<RadioGroup
// key={`activeFilterType-${props.ensembleName}`}
value={props.selections.filterType}
options={Object.values(RealizationFilterType).map((filterType) => {
return {
label: RealizationFilterTypeStringMapping[filterType],
value: filterType,
};
})}
onChange={(_, value) => handleActiveFilterTypeChange(value)}
/>
</Label>
<div
className={resolveClassNames({
hidden: props.selections.filterType !== RealizationFilterType.BY_REALIZATION_NUMBER,
})}
>
<ByRealizationNumberFilter
initialRealizationNumberSelections={actualInitialRealizationNumberSelections}
realizationNumberSelections={props.selections.realizationNumberSelections}
availableRealizationNumbers={props.availableEnsembleRealizations}
selectedIncludeOrExcludeFilter={props.selections.includeOrExcludeFilter}
disabled={
props.selections.filterType !== RealizationFilterType.BY_REALIZATION_NUMBER
}
onFilterChange={handleRealizationNumberFilterChanged}
/>
</div>
<div
className={resolveClassNames({
hidden: props.selections.filterType !== RealizationFilterType.BY_PARAMETER_VALUES,
})}
>
<ByParameterValueFilter
disabled={props.selections.filterType !== RealizationFilterType.BY_PARAMETER_VALUES}
ensembleParameters={props.ensembleParameters}
parameterIdentStringToValueSelectionReadonlyMap={
props.selections.parameterIdentStringToValueSelectionReadonlyMap
}
onFilterChange={handleParameterValueFilterChanged}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { EnsembleRealizationFilter } from "./ensembleRealizationFilter";
export type { EnsembleRealizationFilterSelections } from "./ensembleRealizationFilter";
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading