-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Parameter list filtering component (#404)
Add component for filter a list of Parameter objects. To shorten the list of parameters based on wanted property values/flags
- Loading branch information
1 parent
44881ab
commit 09407b3
Showing
8 changed files
with
492 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { ParameterListFilter } from "./parameterListFilter"; |
97 changes: 97 additions & 0 deletions
97
frontend/src/framework/components/ParameterListFilter/parameterListFilter.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import React from "react"; | ||
|
||
import { Parameter } from "@framework/EnsembleParameters"; | ||
import { SmartNodeSelector, SmartNodeSelectorSelection } from "@lib/components/SmartNodeSelector"; | ||
import { TreeDataNode } from "@lib/components/SmartNodeSelector"; | ||
import { resolveClassNames } from "@lib/utils/resolveClassNames"; | ||
|
||
import { isEqual } from "lodash"; | ||
|
||
// Icons placed here due to limitation of jest for testing utils (cannot import svg) | ||
import checkIcon from "./private-assets/check.svg"; | ||
import segmentIcon from "./private-assets/segment.svg"; | ||
import { | ||
ParameterParentNodeNames, | ||
createTreeDataNodeListFromParameters, | ||
getParametersMatchingSelectedNodes, | ||
} from "./private-utils/smartNodeSelectorUtils"; | ||
|
||
export type InitialParameterFilter = Extract< | ||
(typeof ParameterParentNodeNames)[keyof typeof ParameterParentNodeNames], | ||
"Continuous" | "Discrete" | "Constant" | "Nonconstant" | ||
>; | ||
|
||
export type ParameterListFilterProps = { | ||
parameters: Parameter[]; | ||
initialFilters?: InitialParameterFilter[]; | ||
showTitle?: boolean; | ||
onChange?: (filteredParameters: Parameter[]) => void; | ||
}; | ||
|
||
export const ParameterListFilter: React.FC<ParameterListFilterProps> = (props: ParameterListFilterProps) => { | ||
const smartNodeSelectorId = React.useId(); | ||
const [selectedTags, setSelectedTags] = React.useState<string[]>( | ||
props.initialFilters ?? [ParameterParentNodeNames.IS_NONCONSTANT] | ||
); | ||
const [selectedNodes, setSelectedNodes] = React.useState<string[]>([]); | ||
const [numberOfMatchingParameters, setNumberOfMatchingParameters] = React.useState<number>(0); | ||
const [parameters, setParameters] = React.useState<Parameter[] | null>(null); | ||
const [previousTreeDataNodeList, setPreviousTreeDataNodeList] = React.useState<TreeDataNode[]>([]); | ||
const smartNodeSelectorDelimiter = ":"; | ||
|
||
let newTreeDataNodeList: TreeDataNode[] | null = null; | ||
if (parameters === null || !isEqual(props.parameters, parameters)) { | ||
newTreeDataNodeList = createTreeDataNodeListFromParameters(props.parameters, checkIcon, segmentIcon); | ||
setParameters(props.parameters); | ||
setPreviousTreeDataNodeList(newTreeDataNodeList); | ||
} | ||
const treeDataNodeList = newTreeDataNodeList ? newTreeDataNodeList : previousTreeDataNodeList; | ||
|
||
// Utilizing useEffect to prevent re-render of parent component during rendering of this component | ||
React.useEffect( | ||
function createFilterParameters() { | ||
if (parameters === null || parameters.length === 0) { | ||
setNumberOfMatchingParameters(0); | ||
if (props.onChange) { | ||
props.onChange([]); | ||
} | ||
return; | ||
} | ||
|
||
const filteredParameters = getParametersMatchingSelectedNodes( | ||
parameters, | ||
selectedNodes, | ||
smartNodeSelectorDelimiter | ||
); | ||
setNumberOfMatchingParameters(filteredParameters.length); | ||
if (props.onChange) { | ||
props.onChange(filteredParameters); | ||
} | ||
}, | ||
[selectedNodes, parameters, props.onChange] | ||
); | ||
|
||
function handleSmartNodeSelectorChange(selection: SmartNodeSelectorSelection) { | ||
setSelectedTags(selection.selectedTags); | ||
setSelectedNodes(selection.selectedNodes); | ||
} | ||
|
||
return ( | ||
<div className={props.showTitle ? "mb-2 mt-2" : ""}> | ||
<> | ||
<SmartNodeSelector | ||
id={smartNodeSelectorId} | ||
delimiter={smartNodeSelectorDelimiter} | ||
data={treeDataNodeList} | ||
selectedTags={selectedTags} | ||
label={props.showTitle ? "Parameter filtering" : undefined} | ||
onChange={handleSmartNodeSelectorChange} | ||
placeholder="Add new filter..." | ||
/> | ||
<div className={resolveClassNames("text-right relative w-full mt-2 text-slate-600 text-sm")}> | ||
Number of matches: {numberOfMatchingParameters} | ||
</div> | ||
</> | ||
</div> | ||
); | ||
}; |
1 change: 1 addition & 0 deletions
1
frontend/src/framework/components/ParameterListFilter/private-assets/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions
1
frontend/src/framework/components/ParameterListFilter/private-assets/segment.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
177 changes: 177 additions & 0 deletions
177
...tend/src/framework/components/ParameterListFilter/private-utils/smartNodeSelectorUtils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import { Parameter, ParameterIdent, ParameterType } from "@framework/EnsembleParameters"; | ||
import { TreeDataNode } from "@lib/components/SmartNodeSelector"; | ||
|
||
export const ParameterParentNodeNames = { | ||
NAME: "Name", | ||
GROUP: "Group", | ||
CONTINUOUS: "Continuous", // For Parameter.type === ParameterType.CONTINUOUS | ||
DISCRETE: "Discrete", // For Parameter.type === ParameterType.DISCRETE | ||
IS_CONSTANT: "Constant", // For Parameter.isConstant === true | ||
IS_NONCONSTANT: "Nonconstant", // For Parameter.isConstant === false | ||
IS_LOGARITHMIC: "Logarithmic", // For Parameter.isLogarithmic === true | ||
IS_LINEAR: "Linear", // For Parameter.isLogarithmic === false | ||
} as const; | ||
|
||
export function createAndAddNode(treeNodeDataList: TreeDataNode[], nodeName: string, icon?: string): TreeDataNode { | ||
const newNode: TreeDataNode = { name: nodeName, description: "", icon: icon }; | ||
treeNodeDataList.push(newNode); | ||
return newNode; | ||
} | ||
|
||
export function createTreeDataNodeListFromParameters( | ||
parameters: Parameter[], | ||
checkIcon?: string, | ||
parentIcon?: string | ||
): TreeDataNode[] { | ||
if (parameters.length === 0) { | ||
return []; | ||
} | ||
|
||
const treeDataNodeList: TreeDataNode[] = []; | ||
|
||
const hasContinuousParameter = parameters.some((parameter) => parameter.type === ParameterType.CONTINUOUS); | ||
|
||
// Node for boolean/state properties on top level | ||
createAndAddNode(treeDataNodeList, ParameterParentNodeNames.CONTINUOUS, checkIcon); | ||
createAndAddNode(treeDataNodeList, ParameterParentNodeNames.DISCRETE, checkIcon); | ||
createAndAddNode(treeDataNodeList, ParameterParentNodeNames.IS_CONSTANT, checkIcon); | ||
createAndAddNode(treeDataNodeList, ParameterParentNodeNames.IS_NONCONSTANT, checkIcon); | ||
if (hasContinuousParameter) { | ||
createAndAddNode(treeDataNodeList, ParameterParentNodeNames.IS_LOGARITHMIC, checkIcon); | ||
createAndAddNode(treeDataNodeList, ParameterParentNodeNames.IS_LINEAR, checkIcon); | ||
} | ||
|
||
// Add parameter name and check for group name | ||
const parameterNameSet = new Set<string>(); | ||
const groupNameSet = new Set<string>(); | ||
for (const parameter of parameters) { | ||
parameterNameSet.add(parameter.name); | ||
|
||
if (parameter.groupName) { | ||
groupNameSet.add(parameter.groupName); | ||
} | ||
} | ||
|
||
// Add parameter names | ||
if (parameterNameSet.size !== 0) { | ||
const nameParentNode = createAndAddNode(treeDataNodeList, ParameterParentNodeNames.NAME, parentIcon); | ||
nameParentNode.children = []; | ||
for (const parameterName of parameterNameSet) { | ||
createAndAddNode(nameParentNode.children, parameterName); | ||
} | ||
} | ||
|
||
// Add parameter groups | ||
if (groupNameSet.size !== 0) { | ||
const groupParentNode = createAndAddNode(treeDataNodeList, ParameterParentNodeNames.GROUP, parentIcon); | ||
groupParentNode.children = []; | ||
for (const groupName of groupNameSet) { | ||
createAndAddNode(groupParentNode.children, groupName); | ||
} | ||
} | ||
|
||
return treeDataNodeList; | ||
} | ||
|
||
export function getChildNodeNamesFromParentNodeName( | ||
nodes: string[], | ||
parentNodeName: string, | ||
delimiter = ":" | ||
): string[] { | ||
return nodes | ||
.filter((node) => node.split(delimiter, 1)[0] === parentNodeName) | ||
.map((node) => node.split(delimiter, 2)[1]); | ||
} | ||
|
||
export function getParametersMatchingSelectedNodes( | ||
parameters: Parameter[], | ||
selectedNodes: string[], | ||
delimiter = ":" | ||
): Parameter[] { | ||
// No selection implies no filtering | ||
if (selectedNodes.length === 0) { | ||
return parameters; | ||
} | ||
|
||
const isContinuousSelected = selectedNodes.includes(ParameterParentNodeNames.CONTINUOUS); | ||
const isDiscreteSelected = selectedNodes.includes(ParameterParentNodeNames.DISCRETE); | ||
const isConstantSelected = selectedNodes.includes(ParameterParentNodeNames.IS_CONSTANT); | ||
const isNonConstantSelected = selectedNodes.includes(ParameterParentNodeNames.IS_NONCONSTANT); | ||
const isLogarithmicSelected = selectedNodes.includes(ParameterParentNodeNames.IS_LOGARITHMIC); | ||
const isLinearSelected = selectedNodes.includes(ParameterParentNodeNames.IS_LINEAR); | ||
|
||
// Intersection filtering, i.e. parameter cannot be both continuous and discrete, constant and non-constant, logarithmic and linear | ||
if (isContinuousSelected && isDiscreteSelected) return []; | ||
if (isConstantSelected && isNonConstantSelected) return []; | ||
if (isLogarithmicSelected && isLinearSelected) return []; | ||
|
||
const selectedParameterNames = getChildNodeNamesFromParentNodeName( | ||
selectedNodes, | ||
ParameterParentNodeNames.NAME, | ||
delimiter | ||
); | ||
const selectedParameterGroups = getChildNodeNamesFromParentNodeName( | ||
selectedNodes, | ||
ParameterParentNodeNames.GROUP, | ||
delimiter | ||
); | ||
|
||
const isNoParameterPropertyAmongSelectedNodes = | ||
!isContinuousSelected && | ||
!isDiscreteSelected && | ||
!isConstantSelected && | ||
!isNonConstantSelected && | ||
!isLogarithmicSelected && | ||
!isLinearSelected && | ||
selectedParameterNames.length === 0 && | ||
selectedParameterGroups.length === 0; | ||
if (isNoParameterPropertyAmongSelectedNodes) { | ||
return []; | ||
} | ||
|
||
const selectedEnsembleParameters: Parameter[] = []; | ||
for (const parameter of parameters) { | ||
// Filter by parameter name | ||
if (selectedParameterNames.length !== 0 && !selectedParameterNames.includes(parameter.name)) { | ||
continue; | ||
} | ||
|
||
// Filter by parameter group | ||
if (selectedParameterGroups.length !== 0 && parameter.groupName === null) { | ||
continue; | ||
} | ||
if ( | ||
selectedParameterGroups.length !== 0 && | ||
parameter.groupName !== null && | ||
!selectedParameterGroups.includes(parameter.groupName) | ||
) { | ||
continue; | ||
} | ||
|
||
// Intersection filter by parameter type (continuous/discrete) | ||
if (isContinuousSelected && parameter.type !== ParameterType.CONTINUOUS) continue; | ||
if (isDiscreteSelected && parameter.type !== ParameterType.DISCRETE) continue; | ||
|
||
// Intersection filter by parameter is constant/non-constant | ||
if (isConstantSelected && !parameter.isConstant) continue; | ||
if (isNonConstantSelected && parameter.isConstant) continue; | ||
|
||
// Filter by parameter is logarithmic/linear (only for continuous parameters) | ||
if (isLogarithmicSelected && parameter.type === ParameterType.CONTINUOUS && !parameter.isLogarithmic) continue; | ||
if (isLinearSelected && parameter.type === ParameterType.CONTINUOUS && parameter.isLogarithmic) continue; | ||
|
||
// Prevent duplicates | ||
const parameterIdent = ParameterIdent.fromNameAndGroup(parameter.name, parameter.groupName); | ||
if ( | ||
selectedEnsembleParameters.some((elm) => | ||
ParameterIdent.fromNameAndGroup(elm.name, elm.groupName).equals(parameterIdent) | ||
) | ||
) { | ||
continue; | ||
} | ||
|
||
selectedEnsembleParameters.push(parameter); | ||
} | ||
|
||
return selectedEnsembleParameters; | ||
} |
Oops, something went wrong.